diff --git a/Dockerfile b/Dockerfile index 1c27100..eb0aead 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,14 +16,17 @@ COPY . . # via the /relay nginx proxy at runtime (instead of hardcoding localhost) ENV VITE_NOSTR_RELAYS="" -# Enable mock data mode — no backend API server in this deployment, -# so auth and content use built-in mock/local data instead of timing out +# Enable mock data mode as default — set to false to use the backend API ENV VITE_USE_MOCK_DATA=true # Content origin must match the seeder's ORIGIN so that relay queries find # the seeded data, regardless of how users access the app in their browser ENV VITE_CONTENT_ORIGIN=http://localhost:7777 +# IndeeHub self-hosted backend API (via nginx /api proxy) +ENV VITE_INDEEHUB_API_URL=/api +ENV VITE_INDEEHUB_CDN_URL=/storage + # Build the application RUN npm run build diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..00315cf --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,96 @@ +ENVIRONMENT=local # local | development | production + +# App - Local +PORT=4000 +DOMAIN=localhost:4000 +FRONTEND_URL=http://localhost:3000 + +# DB - API +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_USER=postgres +DATABASE_PASSWORD=local +DATABASE_NAME=indeehub + +# DB - EVENTS/ANALYTICS (POSTHOG) +DATABASE_POSTHOG_HOST=localhost +DATABASE_POSTHOG_PORT=5434 +DATABASE_POSTHOG_USER=postgres +DATABASE_POSTHOG_PASSWORD=local +DATABASE_POSTHOG_NAME=staging + +# Trascoding Queue - Local +QUEUE_HOST=localhost +QUEUE_PORT=6379 +QUEUE_PASSWORD= + +# BTCPay Server - Bitcoin/Lightning Payments +BTCPAY_URL=https://btcpay.yourdomain.com +BTCPAY_STORE_ID= +BTCPAY_API_KEY= +BTCPAY_WEBHOOK_SECRET= + +# User Pool - AWS Cognito +COGNITO_USER_POOL_ID= +COGNITO_CLIENT_ID= + +# Sendgrid - Email Service +SENDGRID_API_KEY= +SENDGRID_SENDER= +SENDGRID_WAITLIST= + +# AWS KEYS +AWS_ACCESS_KEY= +AWS_SECRET_KEY= +AWS_REGION= + +# AWS S3 +S3_PUBLIC_BUCKET_URL= +S3_PUBLIC_BUCKET_NAME= + +# AWS CloudFront +CLOUDFRONT_PRIVATE_KEY= +CLOUDFRONT_KEY_PAIR_ID= + +# Note: Stripe has been removed. All payments are now via BTCPay Server (Lightning). + +# PAY WITH FLASH - ENTHUSIAST SUBSCRIPTION +FLASH_JWT_SECRET_ENTHUSIAST= + +# PAY WITH FLASH - FILM BUFF SUBSCRIPTION +FLASH_JWT_SECRET_FILM_BUFF= + +# PAY WITH FLASH - CINEPHILE SUBSCRIPTION +FLASH_JWT_SECRET_CINEPHILE= + +# PAY WITH FLASH - RSS ADDON SUBSCRIPTION +FLASH_JWT_SECRET_RSS_ADDON= + +# PAY WITH FLASH - VERIFICATION ADDON SUBSCRIPTION +FLASH_JWT_SECRET_VERIFICATION_ADDON= + +# Transcoding API +TRANSCODING_API_KEY= +TRANSCODING_API_URL=http://localhost:4001 + +# Podping - RSS Update Notifications +PODPING_URL=https://podping.cloud/ +PODPING_KEY= +PODPING_USER_AGENT= + +# Retool API KEY - Admin Dashboard +ADMIN_API_KEY= + +# PostHog - Analytics +POSTHOG_API_KEY= + +# Sentry - Error Tracking +SENTRY_ENVIRONMENT= + +# BUYDRM +PRIVATE_AUTH_CERTIFICATE_KEY_ID = +DRM_SECRET_NAME = + +# Project Review +DASHBOARD_REVIEW_URL = https://oneseventech.retool.com/apps/5e86de84-7cdb-11ef-998f-47951c6124a1/Testing%20IndeeHub/film-details?id= +PROJECT_REVIEW_RECIPIENT_EMAIL = diff --git a/backend/.github/dependabot.yml b/backend/.github/dependabot.yml new file mode 100644 index 0000000..d91a92a --- /dev/null +++ b/backend/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: 'npm' # See documentation for possible values + directory: '/' # Location of package manifests + schedule: + interval: 'weekly' diff --git a/backend/.github/workflows/production-bff.yml b/backend/.github/workflows/production-bff.yml new file mode 100644 index 0000000..d4f71f3 --- /dev/null +++ b/backend/.github/workflows/production-bff.yml @@ -0,0 +1,57 @@ +name: Deploy to Production BFF + +on: + push: + branches: [production-bff] + +jobs: + build: + name: Build Image + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_production_bff + IMAGE_TAG: latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + + - name: Tag image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_production_bff + IMAGE_TAG: latest + run: | + CURRENT_SHA=${GITHUB_SHA::8} + docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:${CURRENT_SHA} + + - name: Push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_production_bff + IMAGE_TAG: latest + run: | + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + - name: Restart ECS Cluster + env: + CLUSTER_NAME: indeehub-cluster-production-bff + SERVICE_NAME: indeehub-api-service-production-bff + run: | + aws ecs update-service --cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment diff --git a/backend/.github/workflows/production.yml b/backend/.github/workflows/production.yml new file mode 100644 index 0000000..b7b78bb --- /dev/null +++ b/backend/.github/workflows/production.yml @@ -0,0 +1,57 @@ +name: Deploy to Production + +on: + push: + branches: [production] + +jobs: + build: + name: Build Image + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_production + IMAGE_TAG: latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + + - name: Tag image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_production + IMAGE_TAG: latest + run: | + CURRENT_SHA=${GITHUB_SHA::8} + docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:${CURRENT_SHA} + + - name: Push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_production + IMAGE_TAG: latest + run: | + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + - name: Restart ECS Cluster + env: + CLUSTER_NAME: indeehub-cluster-production + SERVICE_NAME: indeehub-api-service-production + run: | + aws ecs update-service --cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment diff --git a/backend/.github/workflows/staging.yml b/backend/.github/workflows/staging.yml new file mode 100644 index 0000000..9ba2b5d --- /dev/null +++ b/backend/.github/workflows/staging.yml @@ -0,0 +1,101 @@ +name: Deploy to Staging + +on: + push: + branches: [staging] + +jobs: + build: + name: Build Image + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_staging + IMAGE_TAG: latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + + - name: Tag image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_staging + IMAGE_TAG: latest + run: | + CURRENT_SHA=${GITHUB_SHA::8} + docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:${CURRENT_SHA} + + - name: Push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: indeehub_ecr_repo_staging + IMAGE_TAG: latest + run: | + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + - name: Restart ECS Cluster + env: + CLUSTER_NAME: indeehub-cluster-staging + SERVICE_NAME: indeehub-api-service-staging + run: | + aws ecs update-service --cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment + sentry: + name: Deploy to Sentry + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: '18' + + - name: Build and Install Dependencies + run: npm install && npm run build + + - name: Install Sentry CLI + run: npm install -g @sentry/cli + + - name: Inject and Upload Sentry Source Maps + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: indeehub + SENTRY_PROJECT: indeehub-api + run: | + sentry-cli sourcemaps inject --org $SENTRY_ORG --project $SENTRY_PROJECT ./dist + sentry-cli sourcemaps upload --org $SENTRY_ORG --project $SENTRY_PROJECT ./dist + + - name: Set up Sentry Release + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: indeehub + SENTRY_PROJECT: indeehub-api + run: | + sentry-cli releases new "$GITHUB_SHA" + sentry-cli releases set-commits --auto "$GITHUB_SHA" --ignore-missing + + - name: Notify Sentry Deployment + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: indeehub + SENTRY_PROJECT: indeehub-api + run: | + sentry-cli releases deploys "$GITHUB_SHA" new -e staging diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..b259538 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,45 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +*.env + +.terraform/ +*.tfvars + +.eslintcache + +# Sentry Config File +.sentryclirc diff --git a/backend/.husky/commit-msg b/backend/.husky/commit-msg new file mode 100644 index 0000000..9871b20 --- /dev/null +++ b/backend/.husky/commit-msg @@ -0,0 +1 @@ +export COMMIT_MSG=$(cat $1) && echo "$COMMIT_MSG" | npx --no -- commitlint diff --git a/backend/.husky/pre-commit b/backend/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/backend/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/backend/.lintstagedrc b/backend/.lintstagedrc new file mode 100644 index 0000000..cb68a10 --- /dev/null +++ b/backend/.lintstagedrc @@ -0,0 +1,3 @@ +{ + "*": "npm run lint:staged --" +} \ No newline at end of file diff --git a/backend/.nvmrc b/backend/.nvmrc new file mode 100644 index 0000000..d4b00c5 --- /dev/null +++ b/backend/.nvmrc @@ -0,0 +1 @@ +lts/iron diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..9d36f26 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "auto" +} \ No newline at end of file diff --git a/backend/.vscode/launch.json b/backend/.vscode/launch.json new file mode 100644 index 0000000..a7fd00b --- /dev/null +++ b/backend/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Nest Framework", + "args": ["${workspaceFolder}/src/main.ts"], + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register", + "-r", + "tsconfig-paths/register", + "--watch", + "--", + "--inspect" + ], + "restart": true, + "envFile": "${workspaceFolder}/.env", + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "internalConsoleOptions": "openOnSessionStart", + "sourceMaps": true, + "console": "internalConsole", + "outputCapture": "std" + } + ] +} diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 0000000..512da8a --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "sonarlint.connectedMode.project": { + "connectionId": "sonarqube-indeehub", + "projectKey": "IndeeHub_indeehub-api_53ec3bbb-4a99-40e6-9de4-ae1acf6b125d" + } +} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6120552 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM node:20-alpine AS builder +WORKDIR /app + +# Copy package files first for better caching +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +# Copy source and build +COPY . . +RUN npm run build +RUN npm prune --production + +# ── Production image ────────────────────────────────────────── +FROM node:20-alpine AS production +WORKDIR /app + +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules + +EXPOSE 4000 + +# Run TypeORM migrations on startup, then start the API +CMD ["sh", "-c", "npx typeorm migration:run -d dist/database/ormconfig.js 2>/dev/null; export NODE_OPTIONS='--max-old-space-size=1024' && npm run start:prod"] diff --git a/backend/Dockerfile.ffmpeg b/backend/Dockerfile.ffmpeg new file mode 100644 index 0000000..ec358ba --- /dev/null +++ b/backend/Dockerfile.ffmpeg @@ -0,0 +1,25 @@ +FROM node:20-alpine AS builder +WORKDIR /app + +# Copy package files for the worker +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +COPY . . +RUN npm run build +RUN npm prune --production + +# ── Production image with FFmpeg ────────────────────────────── +FROM node:20-alpine AS production +WORKDIR /app + +# Install FFmpeg +RUN apk add --no-cache ffmpeg + +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules + +# The FFmpeg worker runs as a standalone BullMQ consumer +CMD ["node", "dist/ffmpeg-worker/worker.js"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..f01b3d8 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,273 @@ +![](/.swm/images/IH-banner.png) + +# What is indeehub-api? + +`indeehub-api` is the IndeeHub API Backend repository.\ +What stories will you tell?  + +# Table of Contents + +- [Quickstart Guides](#quickstart-guides) + - [MacOS Quickstart](#macos-quickstart) + - [Linux (Unix) Quickstart](#linux-unix-quickstart) + - [Windows Quickstart](#windows-quickstart) +- [Overview](#overview) + - [Requirements](#requirements) + - [Devtool dependencies](#devtool-dependencies) + - [Installed packages](#installed-packages) +- [Running the API](#running-the-api) +- [Recommended VSCode Extensions](#recommended-vscode-extensions) +- [Running DB Migrations](#running-db-migrations) +- [Running Stripe Webhooks locally](#running-stripe-webhooks-locally) +- [SonarQube](#sonarqube) + +# Quickstart Guides + +## MacOS Quickstart + +### Install NVM + +```zsh +brew update +brew install nvm +mkdir ~/.nvm + +echo "export NVM_DIR=~/.nvm\nsource \$(brew --prefix nvm)/nvm.sh" >> .zshrc +source ~/.zshrc +``` + +### Install dependencies + +```zsh +nvm install # this will install the node version set in .nvmrc (lts/hydrogen) +npm i +cp .env.example .env # Add the environment variables +``` + +## Linux (Unix) Quickstart + +### Install NVM + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +``` + +### Install dependencies + +```bash +nvm install # this will install the node version set in .nvmrc (lts/hydrogen) +npm i +cp .env.example .env # And add the environment variables +``` + +## Windows Quickstart + +To avoid EOL and other Windows-related issues, we recommend installing WSL 2 and running the repository on a Linux distro of your choice. + +Follow this guide to install WSL: [https://learn.microsoft.com/en-us/windows/wsl/install](https://github.com/IndeeHub/indeehub-frontend/tree/main#macos-quickstart)                     + +## Overview + +- **TypeScript**: A typed superset of JavaScript designed with large-scale applications in mind. + +- **ESLint**: Static code analysis to help find problems within a codebase. + +- **Prettier**: An opinionated code formatted. + +- **Nest.js**: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. + +- **Swagger**: A tool that helps design, build, document, and consume RESTful Web services. + +- **TypeORM**: An ORM that can run in NodeJS and can be used with TypeScript. + +- **Cypress**: End-to-end testing framework for web applications. + +- **Commitizen**: Conventional commit messages CLI. + +### **Requirements** + +- **NodeJS 18+** + +- **npm (or equivalent)** + +Notes: + +- We recommend the use of Commitizen to use conventional commit messages and linting before commits. + +### **Devtool dependencies** + +- **Cypress**: End-to-end testing framework for web applications. + +### Installed Packages + +- @aws-sdk/client-s3 + +- @nestjs/common + +- @nestjs/config + +- @nestjs/core + +- @nestjs/passport + +- @nestjs/platform-express + +- @nestjs/schedule + +- @nestjs/swagger + +- @nestjs/typeorm + +- @sendgrid/mail + +- @smithy/hash-node + +- @smithy/protocol-http + +- @zbd/node + +- amazon-cognito-identity-js + +- aws-jwt-verify + +- axios + +- class-transformer + +- class-validator + +- jwks-rsa + +- moment + +- passport + +- passport-jwt + +- pg + +- reflect-metadata + +- rxjs + +- stripe + +- typeorm + +- typeorm-naming-strategies + +**DevDependencies** + +- @nestjs/cli + +- @nestjs/schematics + +- @nestjs/testing + +- @types/express + +- @types/jest + +- @types/node + +- @types/supertest + +- @typescript-eslint/eslint-plugin + +- @typescript-eslint/parser + +- eslint + +- eslint-config-prettier + +- eslint-plugin-prettier + +- jest + +- prettier + +- source-map-support + +- supertest + +- ts-jest + +- ts-loader + +- ts-node + +- tsconfig-paths + +- typescript + +  + +# **Running the API** + +```bash +npm run start:dev +# Or if you want to use the debug tool +npm run start:debug +``` + +# Running DB Migrations + +```bash +npm run build +npm run typeorm:generate-migration --name=add-your-migration-name # will generate it based on the differences between the entities and the DB Schema +npm run typeorm:create-migration --name=add-your-migration-name # will create a blank migration + +npm run build # after you finish the migrations +npm run typeorm:run-migrations # will apply the migrations to the current DB +``` + +  + +# Running Stripe Webhooks locally + +## Installing Stripe CLI + +### MacOS + +```zsh +brew install stripe/stripe-cli/stripe +``` + +### Windows + +```bash +scoop bucket add stripe https://github.com/stripe/scoop-stripe-cli.git +scoop install stripe +``` + +### Linux + +```bash +curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg +echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list +sudo apt update +sudo apt install stripe +``` + +## Log in to CLI + +```bash +stripe login +Your pairing code is: enjoy-enough-outwit-win +This pairing code verifies your authentication with Stripe. +Press Enter to open the browser or visit https://dashboard.stripe.com/stripecli/confirm_auth?t=THQdJfL3x12udFkNorJL8OF1iFlN8Az1 +``` + +## Start webhook listener + +``` +stripe listen --forward-to localhost:4242/webhook +``` + +It will output your endpoint secret, and add it to the .env file. + +# SonarQube + +[![Quality gate](https://sonarqube.indeehub.studio/api/project_badges/quality_gate?project=IndeeHub_indeehub-api_53ec3bbb-4a99-40e6-9de4-ae1acf6b125d&token=sqb_b1e2bd689935a9954f40e4b584689c226f741f3e)](https://sonarqube.indeehub.studio/dashboard?id=IndeeHub_indeehub-api_53ec3bbb-4a99-40e6-9de4-ae1acf6b125d) diff --git a/backend/commitlint.config.js b/backend/commitlint.config.js new file mode 100644 index 0000000..5bb286d --- /dev/null +++ b/backend/commitlint.config.js @@ -0,0 +1,29 @@ +// from https://github.com/arvinxx/gitmoji-commit-workflow/tree/master/packages/gitmoji-regex +const isGitmoji = (commit) => + commit.match( + /^(?::\w*:|(?:\ud83c[\udf00-\udfff])|(?:\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]|♻️|🥅|🧱|🤡|🏷️|🚧|♻️|✏️|⚡️|🔥|⬆️|⏪|🦺|🗃️)\s(?\w*)(?:\((?.*)\))?!?:\s(?(?:(?!#).)*(?:(?!\s).))(?:\s\(?(?#\d*)\)?)?$/gm, + ); +const isConventional = (commit) => + commit.match( + /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|merge)(\(.+\))?:\s/, + ); + +module.exports = { + extends: ['@commitlint/config-conventional', 'gitmoji'], + rules: { + 'subject-empty': () => { + const commitMsg = process.env.COMMIT_MSG || ''; + return isGitmoji(commitMsg) ? [0, 'always'] : [2, 'never']; + }, + 'type-empty': () => { + const commitMsg = process.env.COMMIT_MSG || ''; + return isGitmoji(commitMsg) ? [0, 'always'] : [2, 'never']; + }, + 'start-with-gitmoji': () => { + const commitMsg = process.env.COMMIT_MSG || ''; + return isGitmoji(commitMsg) && !isConventional(commitMsg) + ? [2, 'never'] + : [0, 'always']; + }, + }, +}; diff --git a/backend/docker/docker-compose.yml b/backend/docker/docker-compose.yml new file mode 100644 index 0000000..15425fe --- /dev/null +++ b/backend/docker/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + api: + container_name: 'indeehub-api' + image: 'indeehub-api' + environment: + DATABASE_HOST: XXXX.cbjb1ugj4p4e.us-east-1.rds.amazonaws.com + DATABASE_NAME: XXXX + DATABASE_USER: XXXX + DATABASE_PASSWORD: XXXX + DATABASE_PORT: 5432 + build: + context: ../ + dockerfile: Dockerfile + entrypoint: ['npm', 'run', 'start:prod'] + ports: + - 3000:3000 + volumes: + - ./app:/usr/src/app + - /usr/src/app/node_modules diff --git a/backend/docs/nostr-auth/README.md b/backend/docs/nostr-auth/README.md new file mode 100644 index 0000000..2e04777 --- /dev/null +++ b/backend/docs/nostr-auth/README.md @@ -0,0 +1,89 @@ +# Nostr HTTP Auth (NIP-98) + +IndeeHub exposes an HTTP authentication path that accepts requests signed by Nostr keys (NIP-98). Clients sign each request body + method + URL and send it via the `Authorization: Nostr ` header. The server verifies the event, enforces a tight replay window, and makes the caller’s pubkey available on the Express request. + +## Request format +- Header: `Authorization: Nostr `. +- Event requirements: `kind` 27235, `created_at` within ±120s of server time, and the following tags: + - `["method", ""]` + - `["u", ""]` (include scheme, host, path, and query; exclude fragment) + - `["payload", ""]` (omit or leave empty only when the request has no body) + +## Client examples + +### Browser (NIP-07) +```ts +async function fetchWithNostr(url: string, method: string, body?: unknown) { + const payload = body ? JSON.stringify(body) : ''; + const tags = [ + ['method', method], + ['u', url], + ['payload', payload ? sha256(payload) : ''], + ]; + const unsigned = { + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + tags, + content: '', + }; + + const event = await window.nostr.signEvent(unsigned); + const authorization = `Nostr ${btoa(JSON.stringify(event))}`; + + return fetch(url, { + method, + headers: { + 'content-type': 'application/json', + authorization, + }, + body: payload || undefined, + }); +} +``` + +### Node (nostr-tools) +```ts +import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools'; +import crypto from 'node:crypto'; + +const secret = generateSecretKey(); +const pubkey = getPublicKey(secret); + +const payload = JSON.stringify({ ping: 'pong' }); +const unsigned = { + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['u', 'https://api.indeehub.studio/nostr-auth/echo'], + ['method', 'POST'], + ['payload', crypto.createHash('sha256').update(payload).digest('hex')], + ], + content: '', +}; + +const event = finalizeEvent(unsigned, secret); +const authorization = `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`; +``` + +## Server usage +- Apply the guard: `@UseGuards(NostrAuthGuard)` to require Nostr signatures. +- Hybrid mode: `@UseGuards(HybridAuthGuard)` accepts either Nostr (NIP-98) or the existing JWT guard. +- Access the caller: `req.nostrPubkey` and `req.nostrEvent` are populated on successful verification. +- Session bridge: `POST /auth/nostr/session` (guarded by `NostrAuthGuard`) exchanges a signed HTTP request for a 15-minute JWT (`sub = pubkey`) plus refresh token. `POST /auth/nostr/refresh` exchanges a refresh token for a new pair. Use `@UseGuards(NostrSessionJwtGuard)` or `HybridAuthGuard` to accept these JWTs. +- Link an existing user: `POST /auth/nostr/link` with both `JwtAuthGuard` (current user) and `NostrAuthGuard` (NIP-98 header) to attach a pubkey to the user record. `POST /auth/nostr/unlink` removes it. When a pubkey is linked, Nostr session JWTs also include `uid` so downstream code can attach `req.user`. If you need both JWT + Nostr on the same call, send the Nostr signature in `nostr-authorization` (or `x-nostr-authorization`) header so it doesn’t conflict with `Authorization: Bearer ...`. + +Example: +```ts +@Post('protected') +@UseGuards(NostrAuthGuard) +handle(@Req() req: Request) { + return { pubkey: req.nostrPubkey }; +} +``` + +## Troubleshooting +- Clock skew: ensure client and server clocks are within ±2 minutes. +- URL canonicalization: sign the exact scheme/host/path/query received by the server (no fragments). +- Payload hash: hash the raw request body bytes; any whitespace or re-serialization changes will fail verification. +- Headers: set the `Host` and (if behind proxies) `x-forwarded-proto` to the values used when signing the URL. +- Stale signatures: rotate signatures per request; reuse will fail once outside the replay window. diff --git a/backend/docs/nostr-auth/security-checklist.md b/backend/docs/nostr-auth/security-checklist.md new file mode 100644 index 0000000..54cb510 --- /dev/null +++ b/backend/docs/nostr-auth/security-checklist.md @@ -0,0 +1,10 @@ +# Nostr Auth Security Checklist + +- Replay window: enforce ±120s `created_at` tolerance; reject reused or stale events. +- HTTPS only: require TLS termination before the API; never accept plain HTTP in production. +- Canonical URL: sign/verify the exact scheme + host + path + query; strip fragments. +- Payload hashing: hash raw bytes; reject if the `payload` tag hash differs from the received body. +- Rate limiting: apply IP/pubkey-based throttling to mitigate brute force or flood attempts. +- Logging hygiene: avoid persisting raw payloads/signatures; redact PII and secrets from logs. +- Dependency pinning: lock `nostr-tools` and crypto dependencies; track CVEs and update promptly. +- Test coverage: maintain ≥90% coverage for the Nostr auth service/guard and add E2E cases for tamper/replay. diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs new file mode 100644 index 0000000..5cb59e9 --- /dev/null +++ b/backend/eslint.config.mjs @@ -0,0 +1,92 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import unicorn from 'eslint-plugin-unicorn'; +import importPlugin from 'eslint-plugin-import'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; +import eslintPluginPrettier from 'eslint-plugin-prettier'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: [ + '**/node_modules', + '**/dist', + 'commitlint.config.js', + 'eslint.config.mjs', + ], + }, + ...compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:unicorn/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:prettier/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + unicorn, + importPlugin, + eslintPluginPrettier, + }, + settings: { + "import/resolver": { + // You will also need to install and configure the TypeScript resolver + // See also https://github.com/import-js/eslint-import-resolver-typescript#configuration + "typescript": true, + "node": true, + }, + }, + + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + + parserOptions: { + project: './tsconfig.json', + }, + }, + + rules: { + 'semi': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + }, + ], + + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'unicorn/filename-case': [ + 'error', + { + case: 'kebabCase', + }, + ], + + 'unicorn/prevent-abbreviations': [ + 'error', + { + replacements: { + props: false, + }, + }, + ], + + indent: 'off', + }, + }, +]; diff --git a/backend/nest-cli.json b/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..8891b0c --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,22300 @@ +{ + "name": "indeehub-api", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "indeehub-api", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@aws-sdk/client-auto-scaling": "^3.917.0", + "@aws-sdk/client-cognito-identity-provider": "^3.966.0", + "@aws-sdk/client-ecs": "^3.699.0", + "@aws-sdk/client-s3": "^3.917.0", + "@aws-sdk/client-secrets-manager": "^3.950.0", + "@aws-sdk/cloudfront-signer": "^3.966.0", + "@aws-sdk/s3-request-presigner": "^3.654.0", + "@aws-sdk/url-parser": "^3.374.0", + "@aws-sdk/util-format-url": "^3.734.0", + "@logtail/node": "^0.5.0", + "@nestjs/bullmq": "^11.0.4", + "@nestjs/cache-manager": "^3.0.0", + "@nestjs/common": "^11.0.11", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.9", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.6", + "@nestjs/platform-socket.io": "^11.0.10", + "@nestjs/schedule": "^5.0.1", + "@nestjs/swagger": "^11.2.3", + "@nestjs/throttler": "^6.5.0", + "@nestjs/typeorm": "^11.0.0", + "@nestjs/websockets": "^11.0.11", + "@sendgrid/mail": "^8.1.4", + "@sentry/cli": "^2.38.2", + "@sentry/nestjs": "^9.0.1", + "@sentry/profiling-node": "^9.0.1", + "@smithy/hash-node": "^4.2.5", + "@smithy/protocol-http": "^5.0.1", + "@types/jsonwebtoken": "^9.0.10", + "@zbd/node": "^0.6.4", + "amazon-cognito-identity-js": "^6.3.12", + "aws-jwt-verify": "^5.1.1", + "axios": "^1.8.3", + "bullmq": "^5.14.0", + "cache-manager": "^6.4.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dayjs": "^1.11.13", + "fast-xml-parser": "^5.2.0", + "jsonwebtoken": "^9.0.3", + "jwks-rsa": "^3.0.1", + "moment": "^2.30.1", + "nodemailer": "^6.9.0", + "nostr-tools": "^2.19.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.2", + "rss": "^1.2.2", + "rxjs": "^7.8.1", + "socket.io": "^4.8.1", + "stripe": "^17.4.0", + "typeorm": "^0.3.27", + "typeorm-naming-strategies": "^4.1.0" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.5.0", + "@commitlint/cz-commitlint": "^20.1.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "^9.39.2", + "@nestjs/cli": "^11.0.14", + "@nestjs/schematics": "^11.0.1", + "@nestjs/testing": "^11.0.10", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^25.0.6", + "@types/rss": "^0.0.32", + "@types/socket.io": "^3.0.2", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.24.1", + "commitizen": "^4.3.0", + "commitlint-config-gitmoji": "^2.3.1", + "eslint-config-prettier": "^10.0.1", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^56.0.0", + "husky": "^9.1.6", + "jest": "^29.5.0", + "lint-staged": "^16.2.4", + "prettier": "^3.3.3", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", + "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", + "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", + "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-auto-scaling/-/client-auto-scaling-3.917.0.tgz", + "integrity": "sha512-qOvmheh+Qn9sdeUe6dNzQZ5ArVFmjkX62ETBW6llMkfMPODRUQoHour1TaAN9P73I50zMpH/k9uopIF14K2rCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-node": "3.917.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/client-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", + "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/core": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", + "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", + "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", + "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.917.0.tgz", + "integrity": "sha512-rvQ0QamLySRq+Okc0ZqFHZ3Fbvj3tYuWNIlzyEKklNmw5X5PM1idYKlOJflY2dvUGkIqY3lUC9SC2WL+1s7KIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.917.0.tgz", + "integrity": "sha512-n7HUJ+TgU9wV/Z46yR1rqD9hUjfG50AKi+b5UXTlaDlVD8bckg40i77ROCllp53h32xQj/7H0yBIYyphwzLtmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-ini": "3.917.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", + "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", + "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.916.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/token-providers": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.917.0.tgz", + "integrity": "sha512-pZncQhFbwW04pB0jcD5OFv3x2gAddDYCVxyJVixgyhSw7bKCYxqu6ramfq1NxyVpmm+qsw+ijwi/3cCmhUHF/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", + "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/middleware-logger": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", + "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", + "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", + "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@smithy/core": "^3.17.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/nested-clients": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", + "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", + "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/token-providers": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", + "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/util-endpoints": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", + "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-endpoints": "^3.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", + "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-auto-scaling/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", + "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.966.0.tgz", + "integrity": "sha512-NRTyPlIg/NLwWAhY20tWz3gHnxLey/Vp2FvNdWdjHw3mD1yMNlF5zZpNkKDgWLVlN6Yg+4UzMtVDEOfKY/1aXw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/credential-provider-node": "3.966.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.966.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.1", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.2", + "@smithy/middleware-retry": "^4.4.18", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@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.17", + "@smithy/util-defaults-mode-node": "^4.2.20", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/client-sso": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.966.0.tgz", + "integrity": "sha512-hQZDQgqRJclALDo9wK+bb5O+VpO8JcjImp52w9KPSz9XveNRgE9AYfklRJd8qT2Bwhxe6IbnqYEino2wqUMA1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.966.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.1", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.2", + "@smithy/middleware-retry": "^4.4.18", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@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.17", + "@smithy/util-defaults-mode-node": "^4.2.20", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/core": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.966.0.tgz", + "integrity": "sha512-QaRVBHD1prdrFXIeFAY/1w4b4S0EFyo/ytzU+rCklEjMRT7DKGXGoHXTWLGz+HD7ovlS5u+9cf8a/LeSOEMzww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws-sdk/xml-builder": "3.965.0", + "@smithy/core": "^3.20.1", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.966.0.tgz", + "integrity": "sha512-sxVKc9PY0SH7jgN/8WxhbKQ7MWDIgaJv1AoAKJkhJ+GM5r09G5Vb2Vl8ALYpsy+r8b+iYpq5dGJj8k2VqxoQMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.966.0.tgz", + "integrity": "sha512-VTJDP1jOibVtc5pn5TNE12rhqOO/n10IjkoJi8fFp9BMfmh3iqo70Ppvphz/Pe/R9LcK5Z3h0Z4EB9IXDR6kag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.966.0.tgz", + "integrity": "sha512-4oQKkYMCUx0mffKuH8LQag1M4Fo5daKVmsLAnjrIqKh91xmCrcWlAFNMgeEYvI1Yy125XeNSaFMfir6oNc2ODA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/credential-provider-env": "3.966.0", + "@aws-sdk/credential-provider-http": "3.966.0", + "@aws-sdk/credential-provider-login": "3.966.0", + "@aws-sdk/credential-provider-process": "3.966.0", + "@aws-sdk/credential-provider-sso": "3.966.0", + "@aws-sdk/credential-provider-web-identity": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.966.0.tgz", + "integrity": "sha512-wD1KlqLyh23Xfns/ZAPxebwXixoJJCuDbeJHFrLDpP4D4h3vA2S8nSFgBSFR15q9FhgRfHleClycf6g5K4Ww6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.966.0.tgz", + "integrity": "sha512-7QCOERGddMw7QbjE+LSAFgwOBpPv4px2ty0GCK7ZiPJGsni2EYmM4TtYnQb9u1WNHmHqIPWMbZR0pKDbyRyHlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.966.0", + "@aws-sdk/credential-provider-http": "3.966.0", + "@aws-sdk/credential-provider-ini": "3.966.0", + "@aws-sdk/credential-provider-process": "3.966.0", + "@aws-sdk/credential-provider-sso": "3.966.0", + "@aws-sdk/credential-provider-web-identity": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.966.0.tgz", + "integrity": "sha512-q5kCo+xHXisNbbPAh/DiCd+LZX4wdby77t7GLk0b2U0/mrel4lgy6o79CApe+0emakpOS1nPZS7voXA7vGPz4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.966.0.tgz", + "integrity": "sha512-Rv5aEfbpqsQZzxpX2x+FbSyVFOE3Dngome+exNA8jGzc00rrMZEUnm3J3yAsLp/I2l7wnTfI0r2zMe+T9/nZAQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.966.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/token-providers": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.966.0.tgz", + "integrity": "sha512-Yv1lc9iic9xg3ywMmIAeXN1YwuvfcClLVdiF2y71LqUgIOupW8B8my84XJr6pmOQuKzZa++c2znNhC9lGsbKyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.965.0.tgz", + "integrity": "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/middleware-logger": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.965.0.tgz", + "integrity": "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.965.0.tgz", + "integrity": "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.966.0.tgz", + "integrity": "sha512-MvGoy0vhMluVpSB5GaGJbYLqwbZfZjwEZhneDHdPhgCgQqmCtugnYIIjpUw7kKqWGsmaMQmNEgSFf1zYYmwOyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@smithy/core": "^3.20.1", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/nested-clients": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.966.0.tgz", + "integrity": "sha512-FRzAWwLNoKiaEWbYhnpnfartIdOgiaBLnPcd3uG1Io+vvxQUeRPhQIy4EfKnT3AuA+g7gzSCjMG2JKoJOplDtQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.966.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.1", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.2", + "@smithy/middleware-retry": "^4.4.18", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@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.17", + "@smithy/util-defaults-mode-node": "^4.2.20", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.965.0.tgz", + "integrity": "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/token-providers": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.966.0.tgz", + "integrity": "sha512-8k5cBTicTGYJHhKaweO4gL4fud1KDnLS5fByT6/Xbiu59AxYM4E/h3ds+3jxDMnniCE3gIWpEnyfM9khtmw2lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/types": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.965.0.tgz", + "integrity": "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/util-endpoints": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.965.0.tgz", + "integrity": "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-endpoints": "^3.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.965.0.tgz", + "integrity": "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.966.0.tgz", + "integrity": "sha512-vPPe8V0GLj+jVS5EqFz2NUBgWH35favqxliUOvhp8xBdNRkEjiZm5TqitVtFlxS4RrLY3HOndrWbrP5ejbwl1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws-sdk/xml-builder": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.965.0.tgz", + "integrity": "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity-provider/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ecs": { + "version": "3.752.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecs/-/client-ecs-3.752.0.tgz", + "integrity": "sha512-wouvoNVRTvuzKtn53qb6wfsFCHXXCLy/Rpc7aJBSy1MFcZKIFDSsYIyvCL6Z2klifKZ2Ak9YFrvzD2+QFK+XTg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.750.0", + "@aws-sdk/credential-provider-node": "3.750.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.750.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.750.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.4", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.5", + "@smithy/middleware-retry": "^4.0.6", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.6", + "@smithy/util-defaults-mode-node": "^4.0.6", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.917.0.tgz", + "integrity": "sha512-3L73mDCpH7G0koFv3p3WkkEKqC5wn2EznKtNMrJ6hczPIr2Cu6DJz8VHeTZp9wFZLPrIBmh3ZW1KiLujT5Fd2w==", + "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.916.0", + "@aws-sdk/credential-provider-node": "3.917.0", + "@aws-sdk/middleware-bucket-endpoint": "3.914.0", + "@aws-sdk/middleware-expect-continue": "3.917.0", + "@aws-sdk/middleware-flexible-checksums": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-location-constraint": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/middleware-ssec": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/signature-v4-multi-region": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/eventstream-serde-browser": "^4.2.3", + "@smithy/eventstream-serde-config-resolver": "^4.3.3", + "@smithy/eventstream-serde-node": "^4.2.3", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-blob-browser": "^4.2.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/hash-stream-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/md5-js": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.3", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", + "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", + "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", + "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", + "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.917.0.tgz", + "integrity": "sha512-rvQ0QamLySRq+Okc0ZqFHZ3Fbvj3tYuWNIlzyEKklNmw5X5PM1idYKlOJflY2dvUGkIqY3lUC9SC2WL+1s7KIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.917.0.tgz", + "integrity": "sha512-n7HUJ+TgU9wV/Z46yR1rqD9hUjfG50AKi+b5UXTlaDlVD8bckg40i77ROCllp53h32xQj/7H0yBIYyphwzLtmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-ini": "3.917.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", + "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", + "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.916.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/token-providers": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.917.0.tgz", + "integrity": "sha512-pZncQhFbwW04pB0jcD5OFv3x2gAddDYCVxyJVixgyhSw7bKCYxqu6ramfq1NxyVpmm+qsw+ijwi/3cCmhUHF/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", + "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", + "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", + "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.916.0.tgz", + "integrity": "sha512-pjmzzjkEkpJObzmTthqJPq/P13KoNFuEi/x5PISlzJtHofCNcyXeVAQ90yvY2dQ6UXHf511Rh1/ytiKy2A8M0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", + "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@smithy/core": "^3.17.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", + "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", + "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.916.0.tgz", + "integrity": "sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", + "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", + "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-endpoints": "^3.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", + "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", + "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.950.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.950.0.tgz", + "integrity": "sha512-aOI7W+axLah68a0fOLafu8aIrLuGqyuDp6AsgaKlOVNwr2R3a7WaPquNgL9FPzYydiKKVbUkHHFvraicQIQdOw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/credential-provider-node": "3.948.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@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.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", + "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@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.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/core": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", + "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.7", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", + "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", + "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", + "integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/credential-provider-env": "3.947.0", + "@aws-sdk/credential-provider-http": "3.947.0", + "@aws-sdk/credential-provider-login": "3.948.0", + "@aws-sdk/credential-provider-process": "3.947.0", + "@aws-sdk/credential-provider-sso": "3.948.0", + "@aws-sdk/credential-provider-web-identity": "3.948.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz", + "integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.947.0", + "@aws-sdk/credential-provider-http": "3.947.0", + "@aws-sdk/credential-provider-ini": "3.948.0", + "@aws-sdk/credential-provider-process": "3.947.0", + "@aws-sdk/credential-provider-sso": "3.948.0", + "@aws-sdk/credential-provider-web-identity": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", + "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", + "integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.948.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/token-providers": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", + "integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", + "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", + "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.7", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/nested-clients": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", + "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@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.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/token-providers": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", + "integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", + "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.750.0.tgz", + "integrity": "sha512-y0Rx6pTQXw0E61CaptpZF65qNggjqOgymq/RYZU5vWba5DGQ+iqGt8Yq8s+jfBoBBNXshxq8l8Dl5Uq/JTY1wg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.750.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.750.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.750.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.4", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.5", + "@smithy/middleware-retry": "^4.0.6", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.6", + "@smithy/util-defaults-mode-node": "^4.0.6", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/cloudfront-signer": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/cloudfront-signer/-/cloudfront-signer-3.966.0.tgz", + "integrity": "sha512-cS5iQmeRc1YfRWThfA5DGmpQNcgEXn+zFImZf6Xe7sH/TjY7fNLvg9u/g+p0qINP5AKz5ocDkRDnzcqDukCspQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.1", + "@smithy/url-parser": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.750.0.tgz", + "integrity": "sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.4", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core/node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/core/node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.750.0.tgz", + "integrity": "sha512-In6bsG0p/P31HcH4DBRKBbcDS/3SHvEPjfXV8ODPWZO/l3/p7IRoYBdQ07C9R+VMZU2D0+/Sc/DWK/TUNDk1+Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.750.0.tgz", + "integrity": "sha512-wFB9qqfa20AB0dElsQz5ZlZT5o+a+XzpEpmg0erylmGYqEOvh8NQWfDUVpRmQuGq9VbvW/8cIbxPoNqEbPtuWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.750.0.tgz", + "integrity": "sha512-2YIZmyEr5RUd3uxXpxOLD9G67Bibm4I/65M6vKFP17jVMUT+R1nL7mKqmhEVO2p+BoeV+bwMyJ/jpTYG368PCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/credential-provider-env": "3.750.0", + "@aws-sdk/credential-provider-http": "3.750.0", + "@aws-sdk/credential-provider-process": "3.750.0", + "@aws-sdk/credential-provider-sso": "3.750.0", + "@aws-sdk/credential-provider-web-identity": "3.750.0", + "@aws-sdk/nested-clients": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", + "integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", + "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.7", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", + "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", + "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.7", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", + "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@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.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", + "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.750.0.tgz", + "integrity": "sha512-THWHHAceLwsOiowPEmKyhWVDlEUxH07GHSw5AQFDvNQtGKOQl0HSIFO1mKObT2Q2Vqzji9Bq8H58SO5BFtNPRw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.750.0", + "@aws-sdk/credential-provider-http": "3.750.0", + "@aws-sdk/credential-provider-ini": "3.750.0", + "@aws-sdk/credential-provider-process": "3.750.0", + "@aws-sdk/credential-provider-sso": "3.750.0", + "@aws-sdk/credential-provider-web-identity": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.750.0.tgz", + "integrity": "sha512-Q78SCH1n0m7tpu36sJwfrUSxI8l611OyysjQeMiIOliVfZICEoHcLHLcLkiR+tnIpZ3rk7d2EQ6R1jwlXnalMQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.750.0.tgz", + "integrity": "sha512-FGYrDjXN/FOQVi/t8fHSv8zCk+NEvtFnuc4cZUj5OIbM4vrfFc5VaPyn41Uza3iv6Qq9rZg0QOwWnqK8lNrqUw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.750.0", + "@aws-sdk/core": "3.750.0", + "@aws-sdk/token-providers": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.750.0.tgz", + "integrity": "sha512-Nz8zs3YJ+GOTSrq+LyzbbC1Ffpt7pK38gcOyNZv76pP5MswKTUKNYBJehqwa+i7FcFQHsCk3TdhR8MT1ZR23uA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/nested-clients": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.914.0.tgz", + "integrity": "sha512-mHLsVnPPp4iq3gL2oEBamfpeETFV0qzxRHmcnCfEP3hualV8YF8jbXGmwPCPopUPQDpbYDBHYtXaoClZikCWPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.917.0.tgz", + "integrity": "sha512-UPBq1ZP2CaxwbncWSbVqkhYXQrmfNiqAtHyBxi413hjRVZ4JhQ1UyH7pz5yqiG8zx2/+Po8cUD4SDUwJgda4nw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.916.0.tgz", + "integrity": "sha512-CBRRg6slHHBYAm26AWY/pECHK0vVO/peDoNhZiAzUNt4jV6VftotjszEJ904pKGOr7/86CfZxtCnP3CCs3lQjA==", + "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.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", + "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz", + "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.914.0.tgz", + "integrity": "sha512-Mpd0Sm9+GN7TBqGnZg1+dO5QZ/EOYEcDTo7KfvoyrXScMlxvYm9fdrUVMmLdPn/lntweZGV3uNrs+huasGOOTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz", + "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz", + "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.750.0.tgz", + "integrity": "sha512-3H6Z46cmAQCHQ0z8mm7/cftY5ifiLfCjbObrbyyp2fhQs9zk6gCKzIX8Zjhw0RMd93FZi3ebRuKJWmMglf4Itw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-arn-parser": "3.723.0", + "@smithy/core": "^3.1.4", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.914.0.tgz", + "integrity": "sha512-V1Oae/oLVbpNb9uWs+v80GKylZCdsbqs2c2Xb1FsAUPtYeSnxFuAWsF3/2AEMSSpFe0dTC5KyWr/eKl2aim9VQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.750.0.tgz", + "integrity": "sha512-YYcslDsP5+2NZoN3UwuhZGkhAHPSli7HlJHBafBrvjGV/I9f8FuOO1d1ebxGdEP4HyRXUGyh+7Ur4q+Psk0ryw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.4", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.750.0.tgz", + "integrity": "sha512-OH68BRF0rt9nDloq4zsfeHI0G21lj11a66qosaljtEP66PWm7tQ06feKbFkXHT5E1K3QhJW3nVyK8v2fEBY5fg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.750.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.750.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.750.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.4", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.5", + "@smithy/middleware-retry": "^4.0.6", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.6", + "@smithy/util-defaults-mode-node": "^4.0.6", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz", + "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.750.0.tgz", + "integrity": "sha512-G4GNngNQlh9EyJZj2WKOOikX0Fev1WSxTV/XJugaHlpnVriebvi3GzolrgxUpRrcGpFGWjmAxLi/gYxTUla1ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-format-url": "3.734.0", + "@smithy/middleware-endpoint": "^4.0.5", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.5", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.750.0.tgz", + "integrity": "sha512-RA9hv1Irro/CrdPcOEXKwJ0DJYJwYCsauGEdRXihrRfy8MNSR9E+mD5/Fr5Rxjaq5AHM05DYnN3mg/DU6VwzSw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.750.0.tgz", + "integrity": "sha512-X/KzqZw41iWolwNdc8e3RMcNSMR364viHv78u6AefXOO5eRM40c4/LuST1jDzq35/LpnqRhL7/MuixOetw+sFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", + "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/url-parser": { + "version": "3.374.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.374.0.tgz", + "integrity": "sha512-RC3yEj4iqw5vbCmR4IQ3rhmFQilwHtWO1mZ9kRTUxfJCge3TVlrZzj9PRW3hxlYKdu3xZjSvCgX3ip8SFKXtbw==", + "deprecated": "This package has moved to @smithy/url-parser", + "license": "Apache-2.0", + "dependencies": { + "@smithy/url-parser": "^1.0.1", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/url-parser/node_modules/@smithy/querystring-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-1.1.0.tgz", + "integrity": "sha512-Lm/FZu2qW3XX+kZ4WPwr+7aAeHf1Lm84UjNkKyBu16XbmEV7ukfhXni2aIwS2rcVf8Yv5E7wchGGpOFldj9V4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/url-parser/node_modules/@smithy/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/url-parser/node_modules/@smithy/url-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-1.1.0.tgz", + "integrity": "sha512-tpvi761kzboiLNGEWczuybMPCJh6WHB3cz9gWAG95mSyaKXmmX8ZcMxoV+irZfxDqLwZVJ22XTumu32S7Ow8aQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^1.1.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.723.0.tgz", + "integrity": "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz", + "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.734.0.tgz", + "integrity": "sha512-TxZMVm8V4aR/QkW9/NhujvYpPZjUYqzLwSge5imKZbWFR806NP7RMwc5ilVuHF/bMOln/cVHkl42kATElWBvNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz", + "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz", + "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.750.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.750.0.tgz", + "integrity": "sha512-84HJj9G9zbrHX2opLk9eHfDceB+UIHVrmflMzWHpsmo9fDuro/flIBqaVDlE021Osj6qIM0SJJcnL6s23j7JEw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.750.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.914.0.tgz", + "integrity": "sha512-k75evsBD5TcIjedycYS7QXQ98AmOtbnxRJOPtCo0IwYRmy7UvqgS/gBL5SmrIqeV6FDSYRQMgdBxSMp6MLmdew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/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/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@commitlint/cli": { + "version": "19.7.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.7.1.tgz", + "integrity": "sha512-iObGjR1tE/PfDtDTEfd+tnRkB3/HJzpQqRTyofS2MPPkDn1mp3DBC8SoPDayokfAy+xKhF8+bwRCJO25Nea0YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^19.5.0", + "@commitlint/lint": "^19.7.1", + "@commitlint/load": "^19.6.1", + "@commitlint/read": "^19.5.0", + "@commitlint/types": "^19.5.0", + "tinyexec": "^0.3.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "19.7.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.7.1.tgz", + "integrity": "sha512-fsEIF8zgiI/FIWSnykdQNj/0JE4av08MudLTyYHm4FlLWemKoQvPNUYU2M/3tktWcCEyq7aOkDDgtjrmgWFbvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.5.0", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.5.0.tgz", + "integrity": "sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.5.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cz-commitlint": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/cz-commitlint/-/cz-commitlint-20.3.1.tgz", + "integrity": "sha512-B9pWQ3ufGjkkOQ/ToeBbCGzWGR3rpXXyssZlqDVkWKCiNKYKiyvom3AdMB5kI33uqfmDRVhxMlO3emHiCVHH1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^20.3.1", + "@commitlint/load": "^20.3.1", + "@commitlint/types": "^20.3.1", + "chalk": "^5.3.0", + "lodash.isplainobject": "^4.0.6", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "commitizen": "^4.0.3", + "inquirer": "^9.0.0" + } + }, + "node_modules/@commitlint/cz-commitlint/node_modules/@commitlint/config-validator": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.3.1.tgz", + "integrity": "sha512-ErVLC/IsHhcvxCyh+FXo7jy12/nkQySjWXYgCoQbZLkFp4hysov8KS6CdxBB0cWjbZWjvNOKBMNoUVqkmGmahw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cz-commitlint/node_modules/@commitlint/ensure": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.3.1.tgz", + "integrity": "sha512-h664FngOEd7bHAm0j8MEKq+qm2mH+V+hwJiIE2bWcw3pzJMlO0TPKtk0ATyRAtV6jQw+xviRYiIjjSjfajiB5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cz-commitlint/node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cz-commitlint/node_modules/@commitlint/load": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.3.1.tgz", + "integrity": "sha512-YDD9XA2XhgYgbjju8itZ/weIvOOobApDqwlPYCX5NLO/cPtw2UMO5Cmn44Ks8RQULUVI5fUT6roKvyxcoLbNmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.3.1", + "@commitlint/execute-rule": "^20.0.0", + "@commitlint/resolve-extends": "^20.3.1", + "@commitlint/types": "^20.3.1", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cz-commitlint/node_modules/@commitlint/resolve-extends": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.3.1.tgz", + "integrity": "sha512-iGTGeyaoDyHDEZNjD8rKeosjSNs8zYanmuowY4ful7kFI0dnY4b5QilVYaFQJ6IM27S57LAeH5sKSsOHy4bw5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.3.1", + "@commitlint/types": "^20.3.1", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/cz-commitlint/node_modules/@commitlint/types": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.3.1.tgz", + "integrity": "sha512-VmIFV/JkBRhDRRv7N5B7zEUkNZIx9Mp+8Pe65erz0rKycXLsi8Epcw0XJ+btSeRXgTzE7DyOyA9bkJ9mn/yqVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.5.0.tgz", + "integrity": "sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.5.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.5.0.tgz", + "integrity": "sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.5.0.tgz", + "integrity": "sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.5.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "19.7.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.7.1.tgz", + "integrity": "sha512-3IaOc6HVg2hAoGleRK3r9vL9zZ3XY0rf1RsUf6jdQLuaD46ZHnXBiOPTyQ004C4IvYjSWqJwlh0/u2P73aIE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.5.0", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "19.7.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.7.1.tgz", + "integrity": "sha512-LhcPfVjcOcOZA7LEuBBeO00o3MeZa+tWrX9Xyl1r9PMd5FWsEoZI9IgnGqTKZ0lZt5pO3ZlstgnRyY1CJJc9Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^19.7.1", + "@commitlint/parse": "^19.5.0", + "@commitlint/rules": "^19.6.0", + "@commitlint/types": "^19.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "19.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.6.1.tgz", + "integrity": "sha512-kE4mRKWWNju2QpsCWt428XBvUH55OET2N4QKQ0bF85qS/XbsRGG1MiTByDNlEVpEPceMkDr46LNH95DtRwcsfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.5.0", + "@commitlint/execute-rule": "^19.5.0", + "@commitlint/resolve-extends": "^19.5.0", + "@commitlint/types": "^19.5.0", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.5.0.tgz", + "integrity": "sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.5.0.tgz", + "integrity": "sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.5.0", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.5.0.tgz", + "integrity": "sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^19.5.0", + "@commitlint/types": "^19.5.0", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^0.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.5.0.tgz", + "integrity": "sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.5.0", + "@commitlint/types": "^19.5.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.6.0.tgz", + "integrity": "sha512-1f2reW7lbrI0X0ozZMesS/WZxgPa4/wi56vFuJENBmed6mWq5KsheN/nxqnl/C23ioxpPO/PL6tXpiiFy5Bhjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^19.5.0", + "@commitlint/message": "^19.5.0", + "@commitlint/to-lines": "^19.5.0", + "@commitlint/types": "^19.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.5.0.tgz", + "integrity": "sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.5.0.tgz", + "integrity": "sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.5.0.tgz", + "integrity": "sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gitmoji/commit-types": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@gitmoji/commit-types/-/commit-types-1.1.5.tgz", + "integrity": "sha512-8D3FZMRY+gtYpTcHG1SOGmm9CFqxNh6rI9xDoCydxHxnWgqInbdF3nk9gibW5gXA58Hf2cVcJaLEcGOKLRAtmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@gitmoji/gitmoji-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gitmoji/gitmoji-regex/-/gitmoji-regex-1.0.0.tgz", + "integrity": "sha512-+BFXxcWCxn0UIYuG1v5n9SfaCCS8tw95j1x3QsTJRdGGiihRyVLTHiu1wrHlzH3z4nYXKjCKiZFTdWwMjRI+gQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "emoji-regex": "^10", + "gitmojis": "^3" + } + }, + "node_modules/@gitmoji/parser-opts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@gitmoji/parser-opts/-/parser-opts-1.4.0.tgz", + "integrity": "sha512-zzmx/vtpdB/ijjUm7u9OzHNCXWKpSbzVEgVzOzhilMgoTBlUDyInZFUtiCTV+Wf4oCP9nxGa/kQGQFfN+XLH1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gitmoji/gitmoji-regex": "1.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, + "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", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "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", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@logtail/core": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@logtail/core/-/core-0.5.2.tgz", + "integrity": "sha512-mkQqGFwtZ06x2xsj6kOiEZeLSlPeTDArNOEeB9Q1VHxduRHJFInG7soix8+P8xeaoJx+7itvbUySB0XBlnmLSA==", + "license": "ISC", + "dependencies": { + "@logtail/tools": "^0.5.2", + "@logtail/types": "^0.5.2", + "serialize-error": "8.1.0" + } + }, + "node_modules/@logtail/node": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@logtail/node/-/node-0.5.2.tgz", + "integrity": "sha512-pSleAsbq61WdaMzYkmW+kdCfptIWmjhSvNoyAtFFuaIGNXFS2UEmnHPrvzrPxsFgBswePSGl/m1teHTeBxA+qg==", + "license": "ISC", + "dependencies": { + "@logtail/core": "^0.5.2", + "@logtail/types": "^0.5.2", + "@msgpack/msgpack": "^2.5.1", + "@types/stack-trace": "^0.0.33", + "cross-fetch": "^4.0.0", + "minimatch": "^9.0.5", + "serialize-error": "8.1.0", + "stack-trace": "0.0.10" + } + }, + "node_modules/@logtail/node/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@logtail/node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@logtail/tools": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@logtail/tools/-/tools-0.5.2.tgz", + "integrity": "sha512-1Vg0rznoDYXWYDkz8orjKNTjK5f2eUubHN6tfJ2hHKCRRHy7y+TJpIlCQg3BilVcOvMIfiy4RbrcaFTHvDQWTQ==", + "license": "ISC", + "dependencies": { + "@logtail/types": "^0.5.2", + "cross-fetch": "^4.0.0" + } + }, + "node_modules/@logtail/types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@logtail/types/-/types-0.5.2.tgz", + "integrity": "sha512-G3C3XjJPW/LJS0+sanzsNLLqXHAJkhdBR8h4zFUylOtUOevtlneenGNZFjEil+h/GOb3tUySvBuP2wl51gvf0A==", + "license": "ISC" + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nestjs/bull-shared": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", + "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/bullmq": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-11.0.4.tgz", + "integrity": "sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==", + "license": "MIT", + "dependencies": { + "@nestjs/bull-shared": "^11.0.4", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "bullmq": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@nestjs/cache-manager": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.0.tgz", + "integrity": "sha512-csKvxHSQWfC0OiDo0bNEhLqrmYDopHEvRyC81MxV9xFj1AO+rOKocpHa4M1ZGH//6uKFIPGN9oiR0mvZY77APA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", + "cache-manager": ">=6", + "rxjs": "^7.8.1" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.14", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.14.tgz", + "integrity": "sha512-YwP03zb5VETTwelXU+AIzMVbEZKk/uxJL+z9pw0mdG9ogAtqZ6/mpmIM4nEq/NU8D0a7CBRLcMYUmWW/55pfqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/schematics-cli": "19.2.19", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.0", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.103.0", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "21.0.0", + "iterare": "1.2.1", + "load-esm": "1.0.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.9.tgz", + "integrity": "sha512-a00B0BM4X+9z+t3UxJqIZlemIwCQdYoPKrMcM+ky4z3pkqqG1eTWexjs+YXpGObnLnjtMPVKWlcZHp3adDYvUw==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/core/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", + "integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "cors": "2.8.5", + "express": "5.1.0", + "multer": "2.0.2", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/platform-socket.io": { + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.10.tgz", + "integrity": "sha512-39lAjq0+kZRiMuscDcugoG+onPDciM4jhuf8ZDjVcuSwtib1OGwrFtErSzp/KJsmHPSStgapbNev7eFi32uWQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-5.0.1.tgz", + "integrity": "sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==", + "license": "MIT", + "dependencies": { + "cron": "3.5.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.1.tgz", + "integrity": "sha512-PHPAUk4sXkfCxiMacD1JFC+vEyzXjZJRCu1KT2MmG2hrTiMDMk5KtMprro148JUefNuWbVyN0uLTJVSmWVzhoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.1.7", + "@angular-devkit/schematics": "19.1.7", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.7.tgz", + "integrity": "sha512-q0I6L9KTqyQ7D5M8H+fWLT+yjapvMNb7SRdfU6GzmexO66Dpo83q4HDzuDKIPDF29Yl0ELs9ICJqe9yUXh6yDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.7.tgz", + "integrity": "sha512-AP6FvhMybCYs3gs+vzEAzSU1K//AFT3SVTRFv+C3WMO5dLeAHeGzM8I2dxD5EHQQtqIE/8apP6CxGrnpA5YlFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.1.7", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.3.tgz", + "integrity": "sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.1", + "lodash": "4.17.21", + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.30.2" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.10.tgz", + "integrity": "sha512-uZcdnvmHXWnvozYOAwZi1elpRRfqIfYqHglCavjhjcj3cH1MVZkwoTqntW3XOPQlT4lf96InjP1exGaW4B9wUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@nestjs/websockets": { + "version": "11.0.11", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.11.tgz", + "integrity": "sha512-9sNNT/kYA534iaFyZ9MrOXKwQFuJArsMXhT6ywVxaWKQ84lVbV/sDmdmJUe9mzUGLPiHMn+m3oDUO9MiLTEKPA==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-socket.io": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, + "node_modules/@noble/ciphers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.1.tgz", + "integrity": "sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz", + "integrity": "sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.57.1", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", + "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz", + "integrity": "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.36" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.0.tgz", + "integrity": "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.0.tgz", + "integrity": "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fastify": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.1.tgz", + "integrity": "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.0.tgz", + "integrity": "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.0.tgz", + "integrity": "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.0.tgz", + "integrity": "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.1.tgz", + "integrity": "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.1.tgz", + "integrity": "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/instrumentation": "0.57.1", + "@opentelemetry/semantic-conventions": "1.28.0", + "forwarded-parse": "2.1.2", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz", + "integrity": "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.0.tgz", + "integrity": "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.0.tgz", + "integrity": "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.0.tgz", + "integrity": "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.0.tgz", + "integrity": "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.51.0.tgz", + "integrity": "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.0.tgz", + "integrity": "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz", + "integrity": "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz", + "integrity": "sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.0.tgz", + "integrity": "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.0.tgz", + "integrity": "sha512-/NStIcUWUofc11dL7tSgMk25NqvhtbHDCncgm+yc4iJF8Ste2Q/lwUitjfxqj4qWM280uFmBEtcmtMMjbjRU7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1", + "@types/pg": "8.6.1", + "@types/pg-pool": "2.0.6" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis-4": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.0.tgz", + "integrity": "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.0.tgz", + "integrity": "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.0.tgz", + "integrity": "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", + "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", + "integrity": "sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", + "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/instrumentation": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.2.1.tgz", + "integrity": "sha512-QrcWRAwNHXX4nHXB+Q94nfm701gPQsR4zkaxYV6qCiENopRi8yYvXt6FNIvxbuwEiWW5Zid6DoWwIsBKJ/5r5w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz", + "integrity": "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.56.0.tgz", + "integrity": "sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sendgrid/client": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.4.tgz", + "integrity": "sha512-VxZoQ82MpxmjSXLR3ZAE2OWxvQIW2k2G24UeRPr/SYX8HqWLV/8UBN15T2WmjjnEb5XSmFImTJOKDzzSeKr9YQ==", + "license": "MIT", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.7.4" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.4.tgz", + "integrity": "sha512-MUpIZykD9ARie8LElYCqbcBhGGMaA/E6I7fEcG7Hc2An26QJyLtwOaKQ3taGp8xO8BICPJrSKuYV4bDeAJKFGQ==", + "license": "MIT", + "dependencies": { + "@sendgrid/client": "^8.1.4", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sentry-internal/node-cpu-profiler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.1.0.tgz", + "integrity": "sha512-/gPj8ARZ8Jw8gCQWToCiUyLoOxBDP8wuFNx07mAXegYiDa4NcIvo37ZzDWaTG+wjwa/LvCpHxHff6pejt4KOKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "node-abi": "^3.73.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/cli": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.2.tgz", + "integrity": "sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.42.2", + "@sentry/cli-linux-arm": "2.42.2", + "@sentry/cli-linux-arm64": "2.42.2", + "@sentry/cli-linux-i686": "2.42.2", + "@sentry/cli-linux-x64": "2.42.2", + "@sentry/cli-win32-i686": "2.42.2", + "@sentry/cli-win32-x64": "2.42.2" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz", + "integrity": "sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz", + "integrity": "sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==", + "cpu": [ + "arm" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz", + "integrity": "sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz", + "integrity": "sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz", + "integrity": "sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz", + "integrity": "sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz", + "integrity": "sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/core": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.2.0.tgz", + "integrity": "sha512-REnEuneWyv3DkZfr0ZCQOZRCkBxUuWMY7aJ7BwWU9t3CFRUIPO0ePiXb2eZJEkDtalJK+m9pSTDUbChtrzQmLA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/nestjs": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@sentry/nestjs/-/nestjs-9.2.0.tgz", + "integrity": "sha512-Y5g5HWoaDnajdPOGc7HDvFB7NxRt0Niy+mrR6oU5QhN8RGgmX34Xl2ShGkYC5kE6CpG8x5fCLfX4VVl3OpXgWQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "0.57.1", + "@opentelemetry/instrumentation-nestjs-core": "0.44.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@sentry/core": "9.2.0", + "@sentry/node": "9.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@sentry/node": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.2.0.tgz", + "integrity": "sha512-QDOCIs8hTnwPE34FwYL1oIQneqpqyl85MOEfHnv1K7WZ4XYaHMvlJi1vSDr155buFC9K6JkINTw5yJmU1Pi5mA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/instrumentation-amqplib": "^0.46.0", + "@opentelemetry/instrumentation-connect": "0.43.0", + "@opentelemetry/instrumentation-dataloader": "0.16.0", + "@opentelemetry/instrumentation-express": "0.47.0", + "@opentelemetry/instrumentation-fastify": "0.44.1", + "@opentelemetry/instrumentation-fs": "0.19.0", + "@opentelemetry/instrumentation-generic-pool": "0.43.0", + "@opentelemetry/instrumentation-graphql": "0.47.0", + "@opentelemetry/instrumentation-hapi": "0.45.1", + "@opentelemetry/instrumentation-http": "0.57.1", + "@opentelemetry/instrumentation-ioredis": "0.47.0", + "@opentelemetry/instrumentation-kafkajs": "0.7.0", + "@opentelemetry/instrumentation-knex": "0.44.0", + "@opentelemetry/instrumentation-koa": "0.47.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.44.0", + "@opentelemetry/instrumentation-mongodb": "0.51.0", + "@opentelemetry/instrumentation-mongoose": "0.46.0", + "@opentelemetry/instrumentation-mysql": "0.45.0", + "@opentelemetry/instrumentation-mysql2": "0.45.0", + "@opentelemetry/instrumentation-pg": "0.51.0", + "@opentelemetry/instrumentation-redis-4": "0.46.0", + "@opentelemetry/instrumentation-tedious": "0.18.0", + "@opentelemetry/instrumentation-undici": "0.10.0", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.28.0", + "@prisma/instrumentation": "6.2.1", + "@sentry/core": "9.2.0", + "@sentry/opentelemetry": "9.2.0", + "import-in-the-middle": "^1.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.2.0.tgz", + "integrity": "sha512-ksd3M+KXuHt5vsPcqyy77YxVP0yb27J2LD19fasiybOPedb90XjynEk29zVBmW2iEPt8Ddw55FKDNVnHFEbUjw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.28.0" + } + }, + "node_modules/@sentry/profiling-node": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-9.2.0.tgz", + "integrity": "sha512-75RMNzkiKGa4sYUqrnxaayG8yVc/SnGDQ4fKtF4RR+Ayu/U42UwEnLrVjPlGGXvXzdkBJIPKYTE1hPtsOwIbmg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/node-cpu-profiler": "^2.0.0", + "@sentry/core": "9.2.0", + "@sentry/node": "9.2.0" + }, + "bin": { + "sentry-prune-profiler-binaries": "scripts/prune-profiler-binaries.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.2.tgz", + "integrity": "sha512-nc99TseyTwL1bg+T21cyEA5oItNy1XN4aUeyOlXJnvyRW5VSK1oRKRoSM/Iq0KFPuqZMxjBemSZHZCOZbSyBMw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.8", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.3.tgz", + "integrity": "sha512-rcr0VH0uNoMrtgKuY7sMfyKqbHc4GQaQ6Yp4vwgm+Z6psPuOgL+i/Eo/QWdXRmMinL3EgFM0Z1vkfyPyfzLmjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.8.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.3.tgz", + "integrity": "sha512-EcS0kydOr2qJ3vV45y7nWnTlrPmVIMbUFOZbMG80+e2+xePQISX9DrcbRpVRFTS5Nqz3FiEbDcTCAV0or7bqdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.3.tgz", + "integrity": "sha512-GewKGZ6lIJ9APjHFqR2cUW+Efp98xLu1KmN0jOWxQ1TN/gx3HTUPVbLciFD8CfScBj2IiKifqh9vYFRRXrYqXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.3.tgz", + "integrity": "sha512-uQobOTQq2FapuSOlmGLUeGTpvcBLE5Fc7XjERUSk4dxEi4AhTwuyHYZNAvL4EMUp7lzxxkKDFaJ1GY0ovrj0Kg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.3.tgz", + "integrity": "sha512-QIvH/CKOk1BZPz/iwfgbh1SQD5Y0lpaw2kLA8zpLRRtYMPXeYUEWh+moTaJyqDaKlbrB174kB7FSRFiZ735tWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.4.tgz", + "integrity": "sha512-W7eIxD+rTNsLB/2ynjmbdeP7TgxRXprfvqQxKFEfy9HW2HeD7t+g+KCIrY0pIn/GFjA6/fIpH+JQnfg5TTk76Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.3.tgz", + "integrity": "sha512-EXMSa2yiStVII3x/+BIynyOAZlS7dGvI7RFrzXa/XssBgck/7TXJIvnjnCu328GY/VwHDC4VeDyP1S4rqwpYag==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.3.tgz", + "integrity": "sha512-5+4bUEJQi/NRgzdA5SVXvAwyvEnD0ZAiKzV3yLO6dN5BG8ScKBweZ8mxXXUtdxq+Dx5k6EshKk0XJ7vgvIPSnA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.3.tgz", + "integrity": "sha512-Zb8R35hjBhp1oFhiaAZ9QhClpPHdEDmNDC2UrrB2fqV0oNDUUPH12ovZHB5xi/Rd+pg/BJHOR1q+SfsieSKPQg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.2", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-middleware": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.19.tgz", + "integrity": "sha512-QtisFIjIw2tjMm/ESatjWFVIQb5Xd093z8xhxq/SijLg7Mgo2C2wod47Ib/AHpBLFhwYXPzd7Hp2+JVXfeZyMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.10.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.4.tgz", + "integrity": "sha512-rHig+BWjhjlHlah67ryaW9DECYixiJo5pQCTEwsJyarRBAwHMMC3iYz5MXXAHXe64ZAMn1NhTUSTFIu1T6n6jg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.2", + "@smithy/middleware-endpoint": "^4.4.3", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.18", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.18.tgz", + "integrity": "sha512-Ao1oLH37YmLyHnKdteMp6l4KMCGBeZEAN68YYe00KAaKFijFELDbRQRm3CNplz7bez1HifuBV0l5uR6eVJLhIg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.21.tgz", + "integrity": "sha512-e21ASJDirE96kKXZLcYcnn4Zt0WGOvMYc1P8EK0gQeQ3I8PbJWqBKx9AUr/YeFpDkpYwEu1RsPe4UXk2+QL7IA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.5", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/types": "^4.11.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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.3.tgz", + "integrity": "sha512-5+nU///E5sAdD7t3hs4uwvCTWQtTR8JwKwOCSJtBRx0bY1isDo1QwH87vRK86vlFLBTISqoDA2V6xvP6nF1isQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", + "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", + "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/pg": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/rss": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/rss/-/rss-0.0.32.tgz", + "integrity": "sha512-2oKNqKyUY4RSdvl5eZR1n2Q9yvw3XTe3mQHsFPn9alaNBxfPnbXBtGP8R0SV8pK1PrVnLul0zx7izbm5/gF5Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, + "node_modules/@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, + "node_modules/@types/stack-trace": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.33.tgz", + "integrity": "sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==", + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@zbd/node": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@zbd/node/-/node-0.6.4.tgz", + "integrity": "sha512-Dx+nYDlqo3EjbhvUrLgbdtYPQrdeI6yYq+SjJv+MumCOTO2BAZsJSqiS6nPLuqJK80pHch8vAtZKOkVeVduD1w==", + "license": "MIT", + "dependencies": { + "@zbd/node": "^0.6.2", + "node-fetch": "^3.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@zbd/node/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/amazon-cognito-identity-js": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.12.tgz", + "integrity": "sha512-s7NKDZgx336cp+oDeUtB2ZzT8jWJp/v2LWuYl+LQtMEODe22RF1IJ4nRiDATp+rp1pTffCZcm44Quw4jx2bqNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "1.2.2", + "buffer": "4.9.2", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/@aws-crypto/sha256-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", + "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^1.2.2", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/@aws-crypto/util": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", + "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-jwt-verify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/aws-jwt-verify/-/aws-jwt-verify-5.1.1.tgz", + "integrity": "sha512-j6whGdGJmQ27agk4ijY8RPv6itb8JLb7SCJ86fEnneTcSBrpxuwL8kLq6y5WVH95aIknyAloEqAsaOLS1J8ITQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/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/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bullmq": { + "version": "5.41.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.6.tgz", + "integrity": "sha512-4QeYUv3da3+1K9krvvaN0xFKhokr/kFvvf/UbamxezIH923jC+gWj7zcSBYpU+ubo7tV3dHSd9bYoeoTq65e7w==", + "license": "MIT", + "peer": true, + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-manager": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.0.tgz", + "integrity": "sha512-eUmPyVqQYzWCt7hx1QrYzQ7oC3MGKM1etxxe8zuq1o7IB4NzdBeWcUGDSWYahaI8fkd538SEZRGadyZWQfvOzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "keyv": "^5.2.3" + } + }, + "node_modules/cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", + "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commitizen": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz", + "integrity": "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cachedir": "2.3.0", + "cz-conventional-changelog": "3.3.0", + "dedent": "0.7.0", + "detect-indent": "6.1.0", + "find-node-modules": "^2.1.2", + "find-root": "1.1.0", + "fs-extra": "9.1.0", + "glob": "7.2.3", + "inquirer": "8.2.5", + "is-utf8": "^0.2.1", + "lodash": "4.17.21", + "minimist": "1.2.7", + "strip-bom": "4.0.0", + "strip-json-comments": "3.1.1" + }, + "bin": { + "commitizen": "bin/commitizen", + "cz": "bin/git-cz", + "git-cz": "bin/git-cz" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commitizen/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/commitizen/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commitizen/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/commitizen/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/commitizen/node_modules/glob": { + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/commitizen/node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/commitizen/node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commitizen/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/commitizen/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/commitizen/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/commitizen/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/commitizen/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/commitlint-config-gitmoji": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/commitlint-config-gitmoji/-/commitlint-config-gitmoji-2.3.1.tgz", + "integrity": "sha512-T15ssbsyNc6szHlnGWo0/xvIA1mObqM70E9TwKNVTpksxhm+OdFht8hvDdKJAVi4nlZX5tcfTeILOi7SHBGH3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^17", + "@gitmoji/commit-types": "1.1.5", + "@gitmoji/parser-opts": "1.4.0", + "commitlint-plugin-gitmoji": "2.2.6" + } + }, + "node_modules/commitlint-config-gitmoji/node_modules/@commitlint/types": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.8.1.tgz", + "integrity": "sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/commitlint-config-gitmoji/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/commitlint-config-gitmoji/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commitlint-plugin-gitmoji": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/commitlint-plugin-gitmoji/-/commitlint-plugin-gitmoji-2.2.6.tgz", + "integrity": "sha512-oKHPHeNXby0Ix0ZbHVSK5ZyPx1V4fyBjLOy93cYwXhOEPXe36nkDc/HDPFfQpzx1vz39277TaP9LScbqTbscfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^17", + "@gitmoji/gitmoji-regex": "1.0.0", + "gitmojis": "^3" + } + }, + "node_modules/commitlint-plugin-gitmoji/node_modules/@commitlint/types": { + "version": "17.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.8.1.tgz", + "integrity": "sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/commitlint-plugin-gitmoji/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/commitlint-plugin-gitmoji/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commit-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", + "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz", + "integrity": "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.4.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.5.0.tgz", + "integrity": "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.5.0" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cz-conventional-changelog": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", + "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "commitizen": "^4.0.3", + "conventional-commit-types": "^3.0.0", + "lodash.map": "^4.5.1", + "longest": "^2.0.1", + "word-wrap": "^1.0.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@commitlint/load": ">6.1.1" + } + }, + "node_modules/cz-conventional-changelog/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/cz-conventional-changelog/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cz-conventional-changelog/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/cz-conventional-changelog/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.8.3.tgz", + "integrity": "sha512-A0bu4Ks2QqDWNpeEgTQMPTngaMhuDu4yv6xpftBMAf+1ziXnpx+eSR1WRfoPTe2BAiAjHFZ7kSNx1fvr5g5pmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^1.0.2", + "stable-hash": "^0.0.4", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/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/eslint-plugin-import/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "56.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz", + "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "@eslint-community/eslint-utils": "^4.4.0", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.38.1", + "esquery": "^1.6.0", + "globals": "^15.9.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.3", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "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" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-node-modules": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", + "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "findup-sync": "^4.0.0", + "merge": "^2.1.1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=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/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/gitmojis": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/gitmojis/-/gitmojis-3.14.0.tgz", + "integrity": "sha512-wPdXxKWfp2dJE6/ch8TDJjYdXvr+4uV67AEcbImj5QC4DmP6Byje1goJL8WgMPFnLY63eadK6oxtnSjdRFzS6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "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-in-the-middle": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.0.tgz", + "integrity": "sha512-YG86SYDtrL/Yu8JgfWb7kjQ0myLeT1whw6fs/ZHFkXFcbk9zJU9lOCsSJHpvaPumU11nN3US7NW6x1YTk+HrUA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ioredis": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", + "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/glob": { + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz", + "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.0.2" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz", + "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==", + "license": "MIT" + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.4.tgz", + "integrity": "sha512-Pkyr/wd90oAyXk98i/2KwfkIhoYQUMtss769FIT9hFM5ogYZwrk+GRE46yKXSg2ZGhcJ1p38Gf5gmI5Ohjg2yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.4", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", + "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/longest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", + "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "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", + "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/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "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" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nostr-tools": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.3.tgz", + "integrity": "sha512-IJ1fcTYAfVVUbS8S/FtDopdPHCDh/34aHXnyppUzXDYGWlJMv2y1phomvOADZrMAQ6Z28iOIBEyGY256w1492Q==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "^0.5.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1", + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-tools/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "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", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "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.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/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", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "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/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rss": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", + "integrity": "sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg==", + "license": "MIT", + "dependencies": { + "mime-types": "2.1.13", + "xml": "1.0.1" + } + }, + "node_modules/rss/node_modules/mime-db": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", + "integrity": "sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rss/node_modules/mime-types": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", + "integrity": "sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.25.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.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", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stripe": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-17.7.0.tgz", + "integrity": "sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.30.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz", + "integrity": "sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "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": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", + "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.12", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "reflect-metadata": "^0.1.14 || ^0.2.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm-naming-strategies": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/typeorm-naming-strategies/-/typeorm-naming-strategies-4.1.0.tgz", + "integrity": "sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ==", + "license": "MIT", + "peerDependencies": { + "typeorm": "^0.2.0 || ^0.3.0" + } + }, + "node_modules/typeorm/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..2956ca9 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,165 @@ +{ + "name": "indeehub-api", + "version": "0.0.1", + "private": true, + "description": "", + "license": "UNLICENSED", + "author": "", + "scripts": { + "build": "nest build", + "commit": "npx git-cz", + "commit:FORCE": "npx git-cz -n", + "lint": "npx eslint . --cache", + "lint:fix": "npx eslint . --cache --fix", + "lint:staged": "npx eslint --cache --fix --max-warnings 0 --no-warn-ignored", + "prepare": "husky", + "start": "nest start", + "start:debug": "nest start --debug --watch", + "start:dev": "nest start --watch", + "start:prod": "node dist/main", + "test": "jest", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "test:watch": "jest --watch", + "typeorm": "ts-node ./node_modules/typeorm/cli", + "typeorm:create-migration": "npm run typeorm -- migration:create src/database/migrations/$npm_config_name", + "typeorm:generate-migration": "npm run typeorm -- -d src/database/ormconfig.ts migration:generate src/database/migrations/$npm_config_name", + "typeorm:revert-migration": "npm run typeorm -- -d src/database/ormconfig.ts migration:revert", + "typeorm:run-migrations": "npm run typeorm migration:run -- -d src/database/ormconfig.ts" + }, + "config": { + "commitizen": { + "path": "@commitlint/cz-commitlint" + } + }, + "jest": { + "collectCoverage": true, + "collectCoverageFrom": [ + "nostr-auth/nostr-auth.service.ts", + "nostr-auth/nostr-auth.guard.ts", + "auth/nostr-session.service.ts", + "!**/*.spec.ts", + "!**/node_modules/**", + "!**/dist/**" + ], + "coverageDirectory": "../coverage", + "coveragePathIgnorePatterns": [ + "/node_modules/", + "/dist/" + ], + "coverageReporters": [ + "json", + "lcov", + "text", + "clover" + ], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "moduleNameMapper": { + "^src/(.*)$": "/$1" + }, + "rootDir": "src", + "testEnvironment": "node", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.ts$": "ts-jest" + } + }, + "dependencies": { + "@aws-sdk/client-auto-scaling": "^3.917.0", + "@aws-sdk/client-cognito-identity-provider": "^3.966.0", + "@aws-sdk/client-ecs": "^3.699.0", + "@aws-sdk/client-s3": "^3.917.0", + "@aws-sdk/client-secrets-manager": "^3.950.0", + "@aws-sdk/cloudfront-signer": "^3.966.0", + "@aws-sdk/s3-request-presigner": "^3.654.0", + "@aws-sdk/url-parser": "^3.374.0", + "@aws-sdk/util-format-url": "^3.734.0", + "@logtail/node": "^0.5.0", + "@nestjs/bullmq": "^11.0.4", + "@nestjs/cache-manager": "^3.0.0", + "@nestjs/common": "^11.0.11", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.9", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.6", + "@nestjs/platform-socket.io": "^11.0.10", + "@nestjs/schedule": "^5.0.1", + "@nestjs/swagger": "^11.2.3", + "@nestjs/throttler": "^6.5.0", + "@nestjs/typeorm": "^11.0.0", + "@nestjs/websockets": "^11.0.11", + "@sendgrid/mail": "^8.1.4", + "@sentry/cli": "^2.38.2", + "@sentry/nestjs": "^9.0.1", + "@sentry/profiling-node": "^9.0.1", + "@smithy/hash-node": "^4.2.5", + "@smithy/protocol-http": "^5.0.1", + "@types/jsonwebtoken": "^9.0.10", + "@zbd/node": "^0.6.4", + "amazon-cognito-identity-js": "^6.3.12", + "aws-jwt-verify": "^5.1.1", + "axios": "^1.8.3", + "bullmq": "^5.14.0", + "cache-manager": "^6.4.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dayjs": "^1.11.13", + "fast-xml-parser": "^5.2.0", + "jsonwebtoken": "^9.0.3", + "jwks-rsa": "^3.0.1", + "moment": "^2.30.1", + "nodemailer": "^6.9.0", + "nostr-tools": "^2.19.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.2", + "rss": "^1.2.2", + "rxjs": "^7.8.1", + "socket.io": "^4.8.1", + "stripe": "^17.4.0", + "typeorm": "^0.3.27", + "typeorm-naming-strategies": "^4.1.0" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.5.0", + "@commitlint/cz-commitlint": "^20.1.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "^9.39.2", + "@nestjs/cli": "^11.0.14", + "@nestjs/schematics": "^11.0.1", + "@nestjs/testing": "^11.0.10", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^25.0.6", + "@types/rss": "^0.0.32", + "@types/socket.io": "^3.0.2", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.24.1", + "commitizen": "^4.3.0", + "commitlint-config-gitmoji": "^2.3.1", + "eslint-config-prettier": "^10.0.1", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^56.0.0", + "husky": "^9.1.6", + "jest": "^29.5.0", + "lint-staged": "^16.2.4", + "prettier": "^3.3.3", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + } +} diff --git a/backend/sonar-project.properties b/backend/sonar-project.properties new file mode 100644 index 0000000..31735a4 --- /dev/null +++ b/backend/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.projectKey=IndeeHub_indeehub-api +sonar.organization=indeehub + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=indeehub-api +#sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts new file mode 100644 index 0000000..5d68e03 --- /dev/null +++ b/backend/src/admin/admin.controller.ts @@ -0,0 +1,52 @@ +import { + Controller, + Post, + Body, + UseGuards, + Patch, + Param, +} from '@nestjs/common'; +import { AdminService } from './admin.service'; +import { AdminAuthGuard } from 'src/auth/guards/admin.guard'; +import { AdminRegisterDTO } from './dto/admin-register.dto'; +import { AdminBulkRegisterDTO } from './dto/admin-bulk-register.dto'; +import { AdminCreateSubscriptionDTO } from 'src/subscriptions/dto/admin-create-subscription.dto'; +import { UpdateUserTypeDTO } from './dto/update-user-type.dto'; + +@Controller('admin') +export class AdminController { + constructor(private readonly _adminService: AdminService) {} + + @Post('users') + @UseGuards(AdminAuthGuard) + async createUser(@Body() registerRequest: AdminRegisterDTO) { + return await this._adminService.createUser(registerRequest); + } + + @Post('users/batch') + @UseGuards(AdminAuthGuard) + async createUsers(@Body() { users }: AdminBulkRegisterDTO) { + const statuses = this._adminService.createManyUsers(users); + return statuses; + } + + @Post('users/:userId/subscriptions') + @UseGuards(AdminAuthGuard) + async createSubscription( + @Body() createSubscriptionDTO: AdminCreateSubscriptionDTO, + ) { + return await this._adminService.createSubscription(createSubscriptionDTO); + } + + @Patch('users/:userId/type') + @UseGuards(AdminAuthGuard) + async updateUserTypeToFilmmaker( + @Param('userId') userId: string, + @Body() { professionalName }: UpdateUserTypeDTO, + ) { + return await this._adminService.updateAudienceToFilmmaker( + userId, + professionalName, + ); + } +} diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts new file mode 100644 index 0000000..6d85319 --- /dev/null +++ b/backend/src/admin/admin.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { AdminService } from './admin.service'; +import { AdminController } from './admin.controller'; +import { AuthService } from 'src/auth/auth.service'; +import { FilmmakersModule } from 'src/filmmakers/filmmakers.module'; +import { SubscriptionsService } from 'src/subscriptions/subscriptions.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Subscription } from 'src/subscriptions/entities/subscription.entity'; + +@Module({ + controllers: [AdminController], + imports: [FilmmakersModule, TypeOrmModule.forFeature([Subscription])], + providers: [AdminService, AuthService, SubscriptionsService], +}) +export class AdminModule {} diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts new file mode 100644 index 0000000..91e92ab --- /dev/null +++ b/backend/src/admin/admin.service.ts @@ -0,0 +1,137 @@ +import { + BadRequestException, + Inject, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { AdminRegisterDTO } from './dto/admin-register.dto'; +import { AuthService } from 'src/auth/auth.service'; +import { FilmmakersService } from 'src/filmmakers/filmmakers.service'; +import { UsersService } from 'src/users/users.service'; +// Sentry removed +import { SubscriptionsService } from 'src/subscriptions/subscriptions.service'; +import { AdminCreateSubscriptionDTO } from 'src/subscriptions/dto/admin-create-subscription.dto'; + +@Injectable() +export class AdminService { + constructor( + @Inject(AuthService) + private readonly _authService: AuthService, + @Inject(FilmmakersService) + private readonly _filmmakerService: FilmmakersService, + @Inject(UsersService) + private readonly _userService: UsersService, + @Inject(SubscriptionsService) + private readonly _subscriptionsService: SubscriptionsService, + ) {} + + async updateAudienceToFilmmaker(userId: string, professionalName?: string) { + const user = await this._userService.findUsersById(userId); + if (!user) throw new NotFoundException('User not found'); + if (user.filmmaker) + throw new BadRequestException('User is already a filmmaker'); + + await this._filmmakerService.create({ + professionalName: professionalName || user.legalName, + userId: userId, + }); + } + + async createUser(registerRequest: AdminRegisterDTO) { + try { + const user = await this._userService.createUser({ + userDto: { + ...registerRequest, + cognitoId: undefined, + }, + userType: registerRequest.type, + }); + if (registerRequest.type === 'audience') return user; + + await this._filmmakerService.create({ + professionalName: registerRequest.professionalName, + userId: user.id, + }); + return user; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + async createManyUsers(users: AdminRegisterDTO[]) { + const statuses = []; + + for (const newUser of users) { + try { + let user; + let currentStatus = 'error'; + user = await this._userService.findUserByEmail(newUser.email); + if (!user) { + const welcomeAdminTemplateId = 'd-76a659621f1844a5b50bf4b20a393b99'; + const templateData = { + email: newUser.email, + password: newUser.password, + }; + user = await this._userService.createUser({ + userDto: { + ...newUser, + cognitoId: undefined, + }, + templateId: welcomeAdminTemplateId, + data: templateData, + userType: newUser.type, + }); + + // Create filmmaker profile if the user is a filmmaker + if (newUser.type === 'filmmaker') { + await this._filmmakerService.create({ + professionalName: newUser.professionalName, + userId: user.id, + }); + } + + // Create subscription if provided + if ( + newUser.subscription.duration && + newUser.subscription.type && + newUser.subscription.period + ) { + await this._subscriptionsService.adminCreateSubscription({ + userId: user.id, + type: newUser.subscription.type, + period: newUser.subscription.period, + duration: newUser.subscription.duration, + }); + } + currentStatus = 'success'; + } + statuses.push({ + email: newUser.email, + status: currentStatus, + userId: user.id, + reason: + currentStatus === 'error' ? 'email already exists' : undefined, + }); + } catch (error) { + Logger.log(error); + + Logger.error(`Failed to create user ${newUser.email}: ${error.message}`); + + statuses.push({ + email: newUser.email, + status: 'error', + reason: 'unprocessable Content', + }); + } + } + + return statuses; + } + + async createSubscription(createSubscriptionDTO: AdminCreateSubscriptionDTO) { + return await this._subscriptionsService.adminCreateSubscription( + createSubscriptionDTO, + ); + } +} diff --git a/backend/src/admin/dto/admin-bulk-register.dto.ts b/backend/src/admin/dto/admin-bulk-register.dto.ts new file mode 100644 index 0000000..d24311f --- /dev/null +++ b/backend/src/admin/dto/admin-bulk-register.dto.ts @@ -0,0 +1,11 @@ +import { ValidateNested } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { AdminRegisterDTO } from './admin-register.dto'; + +export class AdminBulkRegisterDTO { + @ValidateNested({ each: true }) + @ApiProperty({ type: () => AdminRegisterDTO }) + @Type(() => AdminRegisterDTO) + users: AdminRegisterDTO[]; +} diff --git a/backend/src/admin/dto/admin-register.dto.ts b/backend/src/admin/dto/admin-register.dto.ts new file mode 100644 index 0000000..7294b90 --- /dev/null +++ b/backend/src/admin/dto/admin-register.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { RegisterDTO } from 'src/auth/dto/request/register.dto'; +import { AdminCreateSubscriptionDTO } from 'src/subscriptions/dto/admin-create-subscription.dto'; + +export class AdminRegisterDTO extends RegisterDTO { + @IsNotEmpty() + @IsString() + password: string; + + @IsOptional() + cognitoId: string; + + @IsOptional() + subscription: AdminCreateSubscriptionDTO; +} diff --git a/backend/src/admin/dto/update-user-type.dto.ts b/backend/src/admin/dto/update-user-type.dto.ts new file mode 100644 index 0000000..1cc3d48 --- /dev/null +++ b/backend/src/admin/dto/update-user-type.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator'; + +export class UpdateUserTypeDTO { + // Currently, the only switch from audience type to filmmaker is allowed + @IsEnum(['filmmaker']) + type: 'filmmaker'; + + @IsOptional() + @IsString() + @MaxLength(100) + professionalName?: string; +} diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts new file mode 100644 index 0000000..f61724b --- /dev/null +++ b/backend/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller('') +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts new file mode 100644 index 0000000..68ceaf8 --- /dev/null +++ b/backend/src/app.module.ts @@ -0,0 +1,111 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { ConfigModule } from '@nestjs/config'; +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; +import { DatabaseModule } from './database/database.module'; +import { UploadModule } from './upload/upload.module'; +import { FilmmakersModule } from './filmmakers/filmmakers.module'; +import { WaitlistModule } from './waitlist/waitlist.module'; +import { PaymentModule } from './payment/payment.module'; +import { ScheduleModule } from '@nestjs/schedule'; +import { InvitesModule } from './invites/invites.module'; +import { MailModule } from './mail/mail.module'; +import { MigrationModule } from './migration/migration.module'; +import { SubscriptionsModule } from './subscriptions/subscriptions.module'; +import { AwardIssuersModule } from './award-issuers/award-issuers.module'; +import { FestivalsModule } from './festivals/festivals.module'; +import { GenresModule } from './genres/genres.module'; +import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; +import { RentsModule } from './rents/rents.module'; +import { EventsModule } from './events/events.module'; +import { WebhooksModule } from './webhooks/webhook.module'; +import { ProjectsModule } from './projects/projects.module'; +import { ContentsModule } from './contents/contents.module'; +import { GlobalFilter } from './common/filter/error-exception.filter'; +import { RssModule } from './rss/rss.module'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { TranscodingServerModule } from './transcoding-server/transcoding-server.module'; +import { BullModule } from '@nestjs/bullmq'; +// Sentry removed -- use self-hosted error tracking later +import { PostHogModule } from './posthog/posthog.module'; +import { SecretsManagerModule } from './secrets-manager/secrets-manager.module'; +import { DRMModule } from './drm/drm.module'; +import { AdminModule } from './admin/admin.module'; +import { SeasonModule } from './season/season.module'; +import { DiscountsModule } from './discounts/discounts.module'; +import { DiscountRedemptionModule } from './discount-redemption/discount-redemption.module'; +import { NostrAuthModule } from './nostr-auth/nostr-auth.module'; +import { LibraryModule } from './library/library.module'; + +@Module({ + imports: [ + CacheModule.register({ + isGlobal: true, + ttl: 30, + max: 100, + }), + ThrottlerModule.forRoot([ + { + ttl: 60_000, + limit: 100, + }, + ]), + BullModule.forRootAsync({ + useFactory: () => ({ + connection: { + host: process.env.QUEUE_HOST, + port: Number.parseInt(process.env.QUEUE_PORT, 10), + password: process.env.QUEUE_PASSWORD, + }, + }), + }), + ConfigModule.forRoot(), + ScheduleModule.forRoot(), + MailModule, + PaymentModule, + DatabaseModule, + AuthModule, + UsersModule, + UploadModule, + ProjectsModule, + ContentsModule, + FilmmakersModule, + WaitlistModule, + InvitesModule, + MigrationModule, + SubscriptionsModule, + AwardIssuersModule, + FestivalsModule, + GenresModule, + RentsModule, + EventsModule, + WebhooksModule, + RssModule, + TranscodingServerModule, + PostHogModule, + SecretsManagerModule, + DRMModule, + AdminModule, + SeasonModule, + DiscountsModule, + DiscountRedemptionModule, + NostrAuthModule, + LibraryModule, + ], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_INTERCEPTOR, + useClass: CacheInterceptor, + }, + { + provide: APP_FILTER, + useClass: GlobalFilter, + }, + ], +}) +export class AppModule {} diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts new file mode 100644 index 0000000..71b5d5f --- /dev/null +++ b/backend/src/app.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + // return how long the app has been running + return `App has been running for ${process.uptime()} seconds.`; + } +} diff --git a/backend/src/auth/__tests__/nostr-session.service.spec.ts b/backend/src/auth/__tests__/nostr-session.service.spec.ts new file mode 100644 index 0000000..6a5c42f --- /dev/null +++ b/backend/src/auth/__tests__/nostr-session.service.spec.ts @@ -0,0 +1,38 @@ +import { NostrSessionService } from '../nostr-session.service'; +import { UsersService } from 'src/users/users.service'; + +describe('NostrSessionService', () => { + const originalSecret = process.env.NOSTR_JWT_SECRET; + const originalRefresh = process.env.NOSTR_JWT_REFRESH_SECRET; + const usersService = { + findUserByNostrPubkey: jest.fn(), + } as unknown as UsersService; + + beforeAll(() => { + process.env.NOSTR_JWT_SECRET = 'test-secret'; + process.env.NOSTR_JWT_REFRESH_SECRET = 'test-refresh-secret'; + process.env.NOSTR_JWT_TTL = '60'; + process.env.NOSTR_JWT_REFRESH_TTL = '120'; + }); + + afterAll(() => { + process.env.NOSTR_JWT_SECRET = originalSecret; + process.env.NOSTR_JWT_REFRESH_SECRET = originalRefresh; + }); + + it('issues and verifies session tokens', async () => { + const service = new NostrSessionService(usersService); + const { accessToken, refreshToken, expiresIn } = + await service.issueTokens('pubkey'); + + expect(expiresIn).toBe(60); + + const accessPayload = service.verifyAccessToken(accessToken); + expect(accessPayload.sub).toBe('pubkey'); + expect(accessPayload.typ).toBe('nostr-session'); + + const refreshPayload = service.verifyRefreshToken(refreshToken); + expect(refreshPayload.sub).toBe('pubkey'); + expect(refreshPayload.typ).toBe('nostr-refresh'); + }); +}); diff --git a/backend/src/auth/auth.config.ts b/backend/src/auth/auth.config.ts new file mode 100644 index 0000000..fc8a826 --- /dev/null +++ b/backend/src/auth/auth.config.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthConfig { + public userPoolId: string = process.env.COGNITO_USER_POOL_ID; + public clientId: string = process.env.COGNITO_CLIENT_ID; + public region: string = process.env.AWS_REGION; + public authority = `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`; +} diff --git a/backend/src/auth/auth.controller.spec.ts b/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..95cdfd6 --- /dev/null +++ b/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,47 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; + +import { MailService } from '../mail/mail.service'; +import { UsersService } from '../users/users.service'; +import { FilmmakersService } from '../filmmakers/filmmakers.service'; +import { NostrSessionService } from './nostr-session.service'; + +// Mock MailService +const mockMailService = { + sendEmail: jest.fn(), +}; + +const mockAuthService = {}; + +const mockUsersService = {}; + +const mockFilmmakersService = {}; + +const mockNostrSessionService = {}; + +describe('AuthController', () => { + let controller: AuthController; + + process.env.COGNITO_USER_POOL_ID = 'dummy_user_pool_id'; + process.env.COGNITO_CLIENT_ID = 'dummy_client_id'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { provide: AuthService, useValue: mockAuthService }, + { provide: MailService, useValue: mockMailService }, + { provide: UsersService, useValue: mockUsersService }, + { provide: FilmmakersService, useValue: mockFilmmakersService }, + { provide: NostrSessionService, useValue: mockNostrSessionService }, + ], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts new file mode 100644 index 0000000..ee4ace2 --- /dev/null +++ b/backend/src/auth/auth.controller.ts @@ -0,0 +1,161 @@ +import { + BadRequestException, + Body, + Controller, + Get, + Post, + UseGuards, + Req, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from './guards/jwt.guard'; +import { User } from './user.decorator'; +import { UsersService } from 'src/users/users.service'; +import { FilmmakersService } from 'src/filmmakers/filmmakers.service'; +import { RequestUser } from './dto/request/request-user.interface'; +import { UserDTO } from 'src/users/dto/response/user.dto'; +import { RegisterDTO } from './dto/request/register.dto'; +import { ValidateSessionDTO } from './dto/request/validate-session.dto'; +import { RegisterOneTimePasswordDTO } from './dto/request/register-otp.dto'; +import { Request } from 'express'; +import { NostrAuthGuard } from 'src/nostr-auth/nostr-auth.guard'; +import { NostrSessionService } from './nostr-session.service'; +import { RefreshNostrSessionDto } from './dto/request/refresh-nostr-session.dto'; +import { NostrSessionJwtGuard } from './guards/nostr-session-jwt.guard'; +import { HybridAuthGuard } from './guards/hybrid-auth.guard'; + +@Controller('auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly userService: UsersService, + private readonly filmmakerService: FilmmakersService, + private readonly nostrSessionService: NostrSessionService, + ) {} + + @Post('register') + async register(@Body() registerDTO: RegisterDTO) { + try { + const user = await this.userService.createUser({ + userDto: registerDTO, + userType: registerDTO.type, + }); + + if (registerDTO.type === 'audience') return user; + await this.filmmakerService.create({ + professionalName: registerDTO.professionalName, + userId: user.id, + }); + return user; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + // Register user for OTP flow if not exists, only for audience + @Post('otp/init') + async initOTP(@Body() registerDTO: RegisterOneTimePasswordDTO) { + try { + let user = await this.userService.findUserByEmail(registerDTO.email); + if (user) return { email: user.email }; + // Register user + const cognitoUser = await this.authService.registerUser(registerDTO); + // Create user in the database + user = await this.userService.createUser({ + userDto: { + cognitoId: cognitoUser.UserSub, + email: registerDTO.email, + legalName: registerDTO.email, // Audience not required legal name + }, + userType: 'audience', + }); + + return { email: user.email }; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Get('me') + @UseGuards(HybridAuthGuard) + async getMe(@Req() request: Request) { + const userFromGuard = (request as RequestUser).user; + if (userFromGuard) { + return new UserDTO(userFromGuard); + } + + if (request.nostrPubkey) { + const user = await this.userService.findUserByNostrPubkey( + request.nostrPubkey, + ); + if (user) { + return new UserDTO(user); + } + } + + throw new UnauthorizedException(); + } + + @Post('validate-session') + @UseGuards(JwtAuthGuard) + async validateSession( + @Body() validateSessionDTO: ValidateSessionDTO, + @User() user: RequestUser['user'], + ) { + return this.authService.validateSession(validateSessionDTO, user.email); + } + + @Post('nostr/session') + @UseGuards(NostrAuthGuard) + async createNostrSession(@Req() request: Request) { + if (!request.nostrPubkey) { + throw new UnauthorizedException('Missing Nostr pubkey'); + } + + return await this.nostrSessionService.issueTokens(request.nostrPubkey); + } + + @Post('nostr/refresh') + async refreshNostrSession(@Body() body: RefreshNostrSessionDto) { + const payload = this.nostrSessionService.verifyRefreshToken( + body.refreshToken, + ); + return await this.nostrSessionService.issueTokens(payload.sub); + } + + @Post('nostr/logout') + @UseGuards(NostrSessionJwtGuard) + logout() { + // Stateless JWT; returning success is equivalent to logout for the client. + return { success: true }; + } + + @Post('nostr/link') + @UseGuards(JwtAuthGuard, NostrAuthGuard) + async linkNostrPubkey( + @User() user: RequestUser['user'], + @Req() request: Request, + ) { + if (!request.nostrPubkey) { + throw new UnauthorizedException('Missing Nostr pubkey'); + } + + try { + const updatedUser = await this.userService.linkNostrPubkey( + user.id, + request.nostrPubkey, + ); + return { nostrPubkey: updatedUser.nostrPubkey }; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Post('nostr/unlink') + @UseGuards(JwtAuthGuard) + async unlinkNostrPubkey(@User() user: RequestUser['user']) { + const updated = await this.userService.unlinkNostrPubkey(user.id); + return { nostrPubkey: updated.nostrPubkey }; + } +} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..6c62d3f --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,46 @@ +import { Global, Module, forwardRef } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from './jwt.strategy'; +import { ConfigModule } from '@nestjs/config'; +import { FilmmakersModule } from 'src/filmmakers/filmmakers.module'; +import { SubscriptionsModule } from 'src/subscriptions/subscriptions.module'; +import { NostrAuthModule } from 'src/nostr-auth/nostr-auth.module'; +import { JwtAuthGuard } from './guards/jwt.guard'; +import { TokenAuthGuard } from './guards/token.guard'; +import { HybridAuthGuard } from './guards/hybrid-auth.guard'; +import { NostrSessionService } from './nostr-session.service'; +import { NostrSessionJwtGuard } from './guards/nostr-session-jwt.guard'; +import { UsersModule } from 'src/users/users.module'; + +@Global() +@Module({ + imports: [ + PassportModule.register({ defaultStrategy: 'jwt' }), + ConfigModule, + FilmmakersModule, + SubscriptionsModule, + NostrAuthModule, + forwardRef(() => UsersModule), + ], + providers: [ + JwtStrategy, + AuthService, + JwtAuthGuard, + TokenAuthGuard, + HybridAuthGuard, + NostrSessionService, + NostrSessionJwtGuard, + ], + controllers: [AuthController], + exports: [ + AuthService, + JwtAuthGuard, + TokenAuthGuard, + HybridAuthGuard, + NostrSessionService, + NostrSessionJwtGuard, + ], +}) +export class AuthModule {} diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts new file mode 100644 index 0000000..6680eb1 --- /dev/null +++ b/backend/src/auth/auth.service.ts @@ -0,0 +1,71 @@ +import { + BadRequestException, + Inject, + Injectable, + Logger, +} from '@nestjs/common'; +import { MailService } from 'src/mail/mail.service'; + +/** + * Auth service. + * Cognito has been removed. Authentication is now handled via: + * 1. Nostr NIP-98 HTTP Auth (primary method) + * 2. Nostr session JWT tokens (for subsequent requests) + * + * The Cognito methods are stubbed to maintain API compatibility + * while the migration to fully Nostr-based auth is completed. + */ +@Injectable() +export class AuthService { + private readonly logger = new Logger(AuthService.name); + + constructor(@Inject(MailService) private readonly mailService: MailService) { + this.logger.log( + 'AuthService initialized (Cognito disabled, Nostr-only mode)', + ); + } + + async registerUser(registerRequest: { email: string; password?: string }) { + this.logger.warn( + 'registerUser called -- Cognito is disabled. Use Nostr auth instead.', + ); + return { + UserSub: 'nostr-user', + UserConfirmed: true, + }; + } + + async authenticateUser(user: { email: string; password: string }) { + this.logger.warn( + 'authenticateUser called -- Cognito is disabled. Use Nostr auth instead.', + ); + throw new BadRequestException( + 'Cognito authentication is disabled. Please use Nostr login.', + ); + } + + async verifyMfaCode(session: string, code: string, email: string) { + throw new BadRequestException('MFA is disabled in Nostr-only mode.'); + } + + async validateSession( + request: { code: string; session?: string; type?: string }, + email: string, + ) { + throw new BadRequestException( + 'Session validation via Cognito is disabled. Use Nostr auth.', + ); + } + + async adminConfirmUser(email: string) { + this.logger.warn('adminConfirmUser called -- no-op in Nostr-only mode.'); + return { success: true }; + } + + async deleteUserFromCognito(email: string) { + this.logger.warn( + 'deleteUserFromCognito called -- no-op in Nostr-only mode.', + ); + return {}; + } +} diff --git a/backend/src/auth/dto/request/refresh-nostr-session.dto.ts b/backend/src/auth/dto/request/refresh-nostr-session.dto.ts new file mode 100644 index 0000000..6485608 --- /dev/null +++ b/backend/src/auth/dto/request/refresh-nostr-session.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class RefreshNostrSessionDto { + @IsString() + refreshToken: string; +} diff --git a/backend/src/auth/dto/request/register-otp.dto.ts b/backend/src/auth/dto/request/register-otp.dto.ts new file mode 100644 index 0000000..cdb321d --- /dev/null +++ b/backend/src/auth/dto/request/register-otp.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class RegisterOneTimePasswordDTO { + @IsNotEmpty() + @IsString() + password: string; + + @IsNotEmpty() + @IsEmail() + email: string; +} diff --git a/backend/src/auth/dto/request/register.dto.ts b/backend/src/auth/dto/request/register.dto.ts new file mode 100644 index 0000000..2124dbb --- /dev/null +++ b/backend/src/auth/dto/request/register.dto.ts @@ -0,0 +1,29 @@ +import { + IsNotEmpty, + IsString, + IsEmail, + IsEnum, + IsOptional, +} from 'class-validator'; + +export class RegisterDTO { + @IsNotEmpty() + @IsString() + legalName: string; + + @IsNotEmpty() + @IsOptional() + @IsString() + professionalName?: string; + + @IsNotEmpty() + @IsEmail() + email: string; + + @IsString() + cognitoId: string; + + @IsNotEmpty() + @IsEnum(['filmmaker', 'audience']) + type: 'filmmaker' | 'audience'; +} diff --git a/backend/src/auth/dto/request/request-user.interface.ts b/backend/src/auth/dto/request/request-user.interface.ts new file mode 100644 index 0000000..f478a51 --- /dev/null +++ b/backend/src/auth/dto/request/request-user.interface.ts @@ -0,0 +1,6 @@ +import { Request } from 'express'; +import { User } from 'src/users/entities/user.entity'; + +export interface RequestUser extends Request { + user: User; +} diff --git a/backend/src/auth/dto/request/validate-session.dto.ts b/backend/src/auth/dto/request/validate-session.dto.ts new file mode 100644 index 0000000..a419a89 --- /dev/null +++ b/backend/src/auth/dto/request/validate-session.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsString, IsEnum, IsOptional } from 'class-validator'; + +export class ValidateSessionDTO { + @IsNotEmpty() + @IsString() + code: string; + + @IsString() + @IsOptional() + session?: string; + + @IsNotEmpty() + @IsEnum(['password', 'mfa']) + type: 'password' | 'mfa'; +} diff --git a/backend/src/auth/guards/admin.guard.ts b/backend/src/auth/guards/admin.guard.ts new file mode 100644 index 0000000..10da2f5 --- /dev/null +++ b/backend/src/auth/guards/admin.guard.ts @@ -0,0 +1,38 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Logger, +} from '@nestjs/common'; +import { timingSafeEqual } from 'node:crypto'; + +@Injectable() +export class AdminAuthGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + try { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException('No token provided'); + } + if ( + !timingSafeEqual( + Buffer.from(token), + Buffer.from(process.env.ADMIN_API_KEY), + ) + ) { + throw new UnauthorizedException('Invalid token'); + } + return true; + } catch (error) { + Logger.error(`Error validating token: ${error.message}`); + throw new UnauthorizedException(); + } + } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/backend/src/auth/guards/hybrid-auth.guard.ts b/backend/src/auth/guards/hybrid-auth.guard.ts new file mode 100644 index 0000000..21c46a1 --- /dev/null +++ b/backend/src/auth/guards/hybrid-auth.guard.ts @@ -0,0 +1,93 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { Request } from 'express'; +import { NostrAuthGuard } from '../../nostr-auth/nostr-auth.guard'; +import { JwtAuthGuard } from './jwt.guard'; +import { NostrSessionJwtGuard } from './nostr-session-jwt.guard'; +import { AuthGuard } from '@nestjs/passport'; + +interface GuardResult { + success: boolean; + error?: Error; +} + +@Injectable() +export class HybridAuthGuard extends AuthGuard('hybrid') { + constructor( + private readonly nostrAuthGuard: NostrAuthGuard, + private readonly nostrSessionJwtGuard: NostrSessionJwtGuard, + private readonly jwtAuthGuard: JwtAuthGuard, + ) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authorization = this.normalizeAuthorizationHeader(request); + const isNostrRequest = authorization?.toLowerCase().startsWith('nostr '); + + let nostrResult: GuardResult | undefined; + if (isNostrRequest) { + nostrResult = await this.tryActivate(this.nostrAuthGuard, context); + if (nostrResult.success) { + return true; + } + } + + const nostrJwtResult = await this.tryActivate( + this.nostrSessionJwtGuard, + context, + ); + if (nostrJwtResult.success) { + return true; + } + + const jwtResult = await this.tryActivate(this.jwtAuthGuard, context); + if (jwtResult.success) { + return true; + } + + Logger.log( + `HybridAuthGuard failed: isNostrRequest=${isNostrRequest}, nostrError=${nostrResult?.error?.message}, nostrJwtError=${nostrJwtResult.error?.message}, jwtError=${jwtResult.error?.message}`, + ); + if (isNostrRequest && nostrResult?.error) { + throw nostrResult.error; + } + + if (jwtResult.error) { + throw jwtResult.error; + } + + if (nostrResult?.error) { + throw nostrResult.error; + } + + if (nostrJwtResult.error) { + throw nostrJwtResult.error; + } + + throw new UnauthorizedException('Authentication failed'); + } + + private async tryActivate( + guard: CanActivate, + context: ExecutionContext, + ): Promise { + try { + const result = await guard.canActivate(context); + return { success: Boolean(result) }; + } catch (error) { + return { success: false, error: error as Error }; + } + } + + private normalizeAuthorizationHeader(request: Request): string | undefined { + const header = request.headers.authorization; + return Array.isArray(header) ? header[0] : header; + } +} diff --git a/backend/src/auth/guards/jwt.guard.ts b/backend/src/auth/guards/jwt.guard.ts new file mode 100644 index 0000000..554d429 --- /dev/null +++ b/backend/src/auth/guards/jwt.guard.ts @@ -0,0 +1,27 @@ +import { + ExecutionContext, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +/** + * JWT Auth Guard stub. + * Cognito JWT verification has been removed. + * This guard now always rejects -- all auth should go through + * NostrSessionJwtGuard or NostrAuthGuard via the HybridAuthGuard. + * + * Kept for API compatibility with endpoints that still reference JwtAuthGuard. + */ +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + async canActivate(context: ExecutionContext): Promise { + Logger.warn( + 'JwtAuthGuard.canActivate called -- Cognito is disabled. Use Nostr auth.', + ); + throw new UnauthorizedException( + 'Cognito authentication is disabled. Use Nostr login.', + ); + } +} diff --git a/backend/src/auth/guards/nostr-session-jwt.guard.ts b/backend/src/auth/guards/nostr-session-jwt.guard.ts new file mode 100644 index 0000000..ec25d9a --- /dev/null +++ b/backend/src/auth/guards/nostr-session-jwt.guard.ts @@ -0,0 +1,49 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Request } from 'express'; +import { NostrSessionService } from '../nostr-session.service'; +import { UsersService } from '../../users/users.service'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class NostrSessionJwtGuard extends AuthGuard('nostr-session-jwt') { + constructor( + private readonly sessionService: NostrSessionService, + private readonly usersService: UsersService, + ) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context + .switchToHttp() + .getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException('No token provided'); + } + + try { + const payload = this.sessionService.verifyAccessToken(token); + request.nostrPubkey = payload.sub; + if (payload.uid) { + const user = await this.usersService.findUsersById(payload.uid); + request.user = user; + } + return true; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + throw new UnauthorizedException('Invalid token'); + } + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/backend/src/auth/guards/token.guard.ts b/backend/src/auth/guards/token.guard.ts new file mode 100644 index 0000000..3b730ad --- /dev/null +++ b/backend/src/auth/guards/token.guard.ts @@ -0,0 +1,38 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Logger, +} from '@nestjs/common'; +import { timingSafeEqual } from 'node:crypto'; + +@Injectable() +export class TokenAuthGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + try { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException('No token provided'); + } + if ( + !timingSafeEqual( + Buffer.from(token), + Buffer.from(process.env.TRANSCODING_API_KEY), + ) + ) { + throw new UnauthorizedException('Invalid token'); + } + return true; + } catch (error) { + Logger.error(`Error validating token: ${error.message}`); + throw new UnauthorizedException(); + } + } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..ec62c7b --- /dev/null +++ b/backend/src/auth/jwt.strategy.ts @@ -0,0 +1,31 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, Logger } from '@nestjs/common'; + +/** + * JWT Strategy stub. + * Cognito JWKS verification has been removed. + * This strategy uses a dummy secret and will never actually validate + * because JwtAuthGuard now always rejects. + * All auth goes through NostrSessionJwtGuard or NostrAuthGuard. + */ +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.NOSTR_JWT_SECRET || 'dummy-key-cognito-disabled', + issuer: undefined, + algorithms: ['HS256'], + }); + + Logger.warn( + 'JwtStrategy initialized with dummy config (Cognito disabled)', + 'AuthModule', + ); + } + + public async validate(payload: any) { + return payload; + } +} diff --git a/backend/src/auth/nostr-session.service.ts b/backend/src/auth/nostr-session.service.ts new file mode 100644 index 0000000..6530f60 --- /dev/null +++ b/backend/src/auth/nostr-session.service.ts @@ -0,0 +1,123 @@ +import { + Inject, + Injectable, + Logger, + UnauthorizedException, + forwardRef, +} from '@nestjs/common'; +import { sign, verify } from 'jsonwebtoken'; +import { randomUUID } from 'node:crypto'; +import { UsersService } from 'src/users/users.service'; + +export interface NostrSessionTokens { + accessToken: string; + refreshToken: string; + expiresIn: number; +} + +export interface NostrSessionPayload { + sub: string; + typ: 'nostr-session' | 'nostr-refresh'; + uid?: string; +} + +@Injectable() +export class NostrSessionService { + private readonly logger = new Logger(NostrSessionService.name); + private readonly accessSecret = process.env.NOSTR_JWT_SECRET; + private readonly refreshSecret = + process.env.NOSTR_JWT_REFRESH_SECRET ?? this.accessSecret; + private readonly accessTtlSeconds = Number( + process.env.NOSTR_JWT_TTL ?? 15 * 60, + ); + private readonly refreshTtlSeconds = Number( + process.env.NOSTR_JWT_REFRESH_TTL ?? 60 * 60 * 24 * 14, + ); + + constructor( + @Inject(forwardRef(() => UsersService)) + private readonly usersService: UsersService, + ) { + if (!this.accessSecret) { + throw new Error('NOSTR_JWT_SECRET is not configured'); + } + } + + /** + * Issue JWT tokens for a Nostr pubkey. + * Auto-creates a user record on first login so that /auth/me works immediately. + */ + async issueTokens(pubkey: string): Promise { + let user = await this.usersService.findUserByNostrPubkey(pubkey); + + // Auto-provision a user on first Nostr login + if (!user) { + this.logger.log(`First Nostr login for ${pubkey.slice(0, 12)}… — creating user`); + try { + user = await this.usersService.createNostrUser(pubkey); + } catch (error) { + this.logger.warn( + `Failed to auto-create user for ${pubkey.slice(0, 12)}…: ${error.message}`, + ); + } + } else if (!user.filmmaker) { + // Existing user without filmmaker profile — backfill + this.logger.log(`Backfilling filmmaker for ${pubkey.slice(0, 12)}…`); + try { + user = await this.usersService.ensureFilmmaker(user); + } catch (error) { + this.logger.warn(`Filmmaker backfill failed: ${error.message}`); + } + } + + const payload: NostrSessionPayload = { + sub: pubkey, + typ: 'nostr-session', + uid: user?.id, + }; + const refreshPayload: NostrSessionPayload = { + sub: pubkey, + typ: 'nostr-refresh', + uid: user?.id, + }; + + const accessToken = sign(payload, this.accessSecret, { + expiresIn: this.accessTtlSeconds, + }); + + const refreshToken = sign(refreshPayload, this.refreshSecret, { + expiresIn: this.refreshTtlSeconds, + }); + + return { + accessToken, + refreshToken, + expiresIn: this.accessTtlSeconds, + }; + } + + verifyAccessToken(token: string): NostrSessionPayload { + return this.verifyToken(token, this.accessSecret, 'nostr-session'); + } + + verifyRefreshToken(token: string): NostrSessionPayload { + return this.verifyToken(token, this.refreshSecret, 'nostr-refresh'); + } + + private verifyToken( + token: string, + secret: string, + expectedType: NostrSessionPayload['typ'], + ): NostrSessionPayload { + try { + const payload = verify(token, secret) as NostrSessionPayload; + if (payload.typ !== expectedType) { + throw new UnauthorizedException('Invalid token type'); + } + return payload; + } catch (error) { + if (error instanceof UnauthorizedException) throw error; + throw new UnauthorizedException('Invalid or expired token'); + } + } +} diff --git a/backend/src/auth/user.decorator.ts b/backend/src/auth/user.decorator.ts new file mode 100644 index 0000000..8a0e4a3 --- /dev/null +++ b/backend/src/auth/user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const User = createParamDecorator( + (data: unknown, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + return request.user; + }, +); diff --git a/backend/src/award-issuers/award-issuers.controller.ts b/backend/src/award-issuers/award-issuers.controller.ts new file mode 100644 index 0000000..e3851a7 --- /dev/null +++ b/backend/src/award-issuers/award-issuers.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; + +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { AwardIssuersService } from './award-issuers.service'; +import { AwardIssuerDTO } from './dto/response/award-issuer.dto'; + +@Controller('award-issuers') +@UseGuards(HybridAuthGuard) +export class AwardIssuersController { + constructor(private readonly issuersService: AwardIssuersService) {} + + @Get() + async findAll() { + const issuers = await this.issuersService.findAll(); + return issuers.map((issuer) => new AwardIssuerDTO(issuer)); + } +} diff --git a/backend/src/award-issuers/award-issuers.module.ts b/backend/src/award-issuers/award-issuers.module.ts new file mode 100644 index 0000000..a03e20f --- /dev/null +++ b/backend/src/award-issuers/award-issuers.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AwardIssuer } from './entities/award-issuer.entity'; +import { AwardIssuersController } from './award-issuers.controller'; +import { AwardIssuersService } from './award-issuers.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([AwardIssuer])], + controllers: [AwardIssuersController], + providers: [AwardIssuersService], +}) +export class AwardIssuersModule {} diff --git a/backend/src/award-issuers/award-issuers.service.ts b/backend/src/award-issuers/award-issuers.service.ts new file mode 100644 index 0000000..9041281 --- /dev/null +++ b/backend/src/award-issuers/award-issuers.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AwardIssuer } from './entities/award-issuer.entity'; + +@Injectable() +export class AwardIssuersService { + constructor( + @InjectRepository(AwardIssuer) + private issuersRepository: Repository, + ) {} + + findAll() { + return this.issuersRepository.find(); + } +} diff --git a/backend/src/award-issuers/dto/response/award-issuer.dto.ts b/backend/src/award-issuers/dto/response/award-issuer.dto.ts new file mode 100644 index 0000000..ea353d7 --- /dev/null +++ b/backend/src/award-issuers/dto/response/award-issuer.dto.ts @@ -0,0 +1,11 @@ +import { AwardIssuer } from 'src/award-issuers/entities/award-issuer.entity'; + +export class AwardIssuerDTO { + id: string; + name: string; + + constructor(awardIssuer: AwardIssuer) { + this.id = awardIssuer.id; + this.name = awardIssuer.name; + } +} diff --git a/backend/src/award-issuers/entities/award-issuer.entity.ts b/backend/src/award-issuers/entities/award-issuer.entity.ts new file mode 100644 index 0000000..74dcd1e --- /dev/null +++ b/backend/src/award-issuers/entities/award-issuer.entity.ts @@ -0,0 +1,27 @@ +import { Award } from 'src/projects/entities/award.entity'; +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('award_issuers') +export class AwardIssuer { + @PrimaryColumn() + id: string; + + @Column() + name: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @OneToMany(() => Award, (award) => award.issuer) + awards: Award[]; +} diff --git a/backend/src/common/constants.ts b/backend/src/common/constants.ts new file mode 100644 index 0000000..9337aa9 --- /dev/null +++ b/backend/src/common/constants.ts @@ -0,0 +1,2 @@ +export const NANOID_LENGTH = 16; +export const PRICE_PER_SECOND = 0.000_208_3; diff --git a/backend/src/common/decorators/throttle.decorator.ts b/backend/src/common/decorators/throttle.decorator.ts new file mode 100644 index 0000000..dc8568e --- /dev/null +++ b/backend/src/common/decorators/throttle.decorator.ts @@ -0,0 +1,43 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +/** + * A custom NestJS decorator to extract rate-limiting parameters + * from the response headers. + * + * **Note:** This only works if `Throttle` from `@nestjs/throttler` + * has been imported and used in the controller or application. + * + * @example + * ```typescript + * import { Throttle } from '@nestjs/throttler'; + * import { ThrottleParameters } from './throttle-parameters.decorator'; + * + * @Throttle(10, 60) // 10 requests per minute + * @Controller('example') + * export class ExampleController { + * @Get() + * exampleEndpoint(@ThrottleParameters() throttleParams) { + * console.log(throttleParams); + * } + * } + * ``` + */ +export const ThrottleParameters = createParamDecorator( + (data: unknown, context: ExecutionContext) => { + const response = context.switchToHttp().getResponse(); + const headers = response.getHeaders(); + const parameters: ThrottleParametersInterface = { + limit: headers['x-ratelimit-limit'], + remaining: headers['x-ratelimit-remaining'], + totalHits: + headers['x-ratelimit-limit'] - headers['x-ratelimit-remaining'], + }; + return parameters; + }, +); + +export interface ThrottleParametersInterface { + limit: number; + remaining: number; + totalHits: number; +} diff --git a/backend/src/common/enums/filter-date-range.enum.ts b/backend/src/common/enums/filter-date-range.enum.ts new file mode 100644 index 0000000..2be8191 --- /dev/null +++ b/backend/src/common/enums/filter-date-range.enum.ts @@ -0,0 +1,8 @@ +export const filterDateRange = [ + 'last_7_days', + 'last_28_days', + 'last_60_days', + 'last_365_days', + 'since_uploaded', +] as const; +export type FilterDateRange = (typeof filterDateRange)[number]; diff --git a/backend/src/common/filter/error-exception.filter.ts b/backend/src/common/filter/error-exception.filter.ts new file mode 100644 index 0000000..7f4d82a --- /dev/null +++ b/backend/src/common/filter/error-exception.filter.ts @@ -0,0 +1,40 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() +export class GlobalFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + Logger.error(exception); + const context = host.switchToHttp(); + const response = context.getResponse(); + + if (exception instanceof HttpException) { + const status = exception.getStatus(); + const responsePayload = exception.getResponse(); + response.status(status).json({ + message: responsePayload['message'], + error: responsePayload['error'], + statusCode: status, + }); + } else { + // There is no more error handlers so it should have status code 500 + const status = HttpStatus.INTERNAL_SERVER_ERROR; + const statusMessage = HttpStatus[status] + .toLowerCase() + .replaceAll('_', ' '); + const errorMessage = + statusMessage.charAt(0).toUpperCase() + statusMessage.slice(1); + response.status(status).json({ + message: errorMessage, + statusCode: status, + }); + } + } +} diff --git a/backend/src/common/guards/user-throttle.guard.ts b/backend/src/common/guards/user-throttle.guard.ts new file mode 100644 index 0000000..fff2f2f --- /dev/null +++ b/backend/src/common/guards/user-throttle.guard.ts @@ -0,0 +1,9 @@ +import { ThrottlerGuard } from '@nestjs/throttler'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UserThrottlerGuard extends ThrottlerGuard { + protected getTracker(request: Record) { + return request.user ? request.user.id : request.ip; + } +} diff --git a/backend/src/common/helper.ts b/backend/src/common/helper.ts new file mode 100644 index 0000000..e2a5653 --- /dev/null +++ b/backend/src/common/helper.ts @@ -0,0 +1,73 @@ +import { FilterDateRange } from 'src/payment/enums/filter-date-range.enum'; +import { Project } from 'src/projects/entities/project.entity'; + +export function getFileRoute(fileKey: string): string { + // Remove the name of the file from the key (last /). + // Example: 'folder-1/folder-2/file.mp4' -> 'folder-1/folder-2/' + return fileKey.slice(0, Math.max(0, fileKey.lastIndexOf('/') + 1)); +} + +export function encodeS3KeyForUrl(fileKey: string): string { + return fileKey + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/'); +} + +export function getPublicS3Url(fileKey: string): string { + return `${process.env.S3_PUBLIC_BUCKET_URL}${encodeS3KeyForUrl(fileKey)}`; +} + +export function getPrivateS3Url(fileKey: string): string { + return `${process.env.S3_PRIVATE_BUCKET_URL}${encodeS3KeyForUrl(fileKey)}`; +} + +export function getTranscodedFileRoute(fileKey: string): string { + const fileRoute = getFileRoute(fileKey); + return `${fileRoute}transcoded/file.m3u8`; +} + +export function getTrailerTranscodeOutputKey(fileKey: string): string { + const fileRoute = getFileRoute(fileKey); + return `${fileRoute}trailer/transcoded`; +} + +export function getTrailerTranscodedFileRoute(fileKey: string): string { + const outputKey = getTrailerTranscodeOutputKey(fileKey); + return `${outputKey}/file.m3u8`; +} + +export function isProjectRSSReady(project: Project) { + return ( + project.status === 'published' && + project.contents.some((content) => content.isRssEnabled) + ); +} + +export const getDaysCount = (dateRange: FilterDateRange) => { + switch (dateRange) { + case 'last_7_days': { + return 7; + } + case 'last_28_days': { + return 28; + } + case 'last_60_days': { + return 60; + } + case 'last_365_days': { + return 365; + } + case 'since_uploaded': + } +}; + +export const getFilterDateRange = (dateRange: FilterDateRange) => { + const currentDate = new Date(); + const days = getDaysCount(dateRange); + const startDate = days + ? new Date(currentDate.setDate(currentDate.getDate() - days)) + : new Date(0); + + return { startDate, endDate: new Date() }; +}; diff --git a/backend/src/contents/contents.controller.ts b/backend/src/contents/contents.controller.ts new file mode 100644 index 0000000..040b440 --- /dev/null +++ b/backend/src/contents/contents.controller.ts @@ -0,0 +1,100 @@ +import { + Controller, + Get, + Param, + UseGuards, + Query, + Patch, + Body, + UseInterceptors, + Post, +} from '@nestjs/common'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator'; +import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard'; +import { ContentsService } from './contents.service'; +import { ContentDTO } from './dto/response/content.dto'; +import { ListContentsDTO } from './dto/request/list-contents.dto'; +import { BaseContentDTO } from './dto/response/base-content.dto'; +import { UpdateContentDTO } from './dto/request/update-content.dto'; +import { PermissionGuard } from 'src/projects/guards/permission.guard'; +import { Roles } from 'src/projects/decorators/roles.decorator'; +import { ContentsInterceptor } from './interceptor/contents.interceptor'; +import { TokenAuthGuard } from 'src/auth/guards/token.guard'; +import { StreamContentDTO } from './dto/response/stream-content.dto'; +import { TranscodingCompletedDTO } from './dto/request/transcoding-completed.dto'; + +@Controller('contents') +export class ContentsController { + constructor(private readonly contentsService: ContentsService) {} + + @Get(':id') + @UseGuards(HybridAuthGuard) + async find(@Param('id') id: string) { + const content = await this.contentsService.findOne(id); + return new BaseContentDTO(content); + } + + @Get('project/:id') + @UseGuards(HybridAuthGuard) + async findAll(@Param('id') id: string, @Query() query: ListContentsDTO) { + const contents = await this.contentsService.findAll(id, query); + return contents.map((project) => new BaseContentDTO(project)); + } + + @Patch('project/:id') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @UseInterceptors(ContentsInterceptor) + @Subscriptions(['filmmaker']) + @Roles([ + 'owner', + 'admin', + 'editor', + 'revenue-manager', + 'cast-crew', + 'shareholder', + 'viewer', + ]) + async upsert(@Param('id') id: string, @Body() body: UpdateContentDTO) { + const { updatedContent } = await this.contentsService.upsert(id, body); + return new ContentDTO(updatedContent); + } + + @Get(':id/stream') + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @Subscriptions(['enthusiast', 'film-buff', 'cinephile']) + async stream(@Param('id') id: string) { + return new StreamContentDTO(await this.contentsService.stream(id)); + } + + @Post(':id/transcoding') + @UseGuards(TokenAuthGuard) + async transcoding( + @Param('id') id: string, + @Body() body: TranscodingCompletedDTO, + ) { + await this.contentsService.transcodingCompleted( + id, + body.status, + body.metadata, + ); + } + + @Post(':id/trailer/transcoding') + @UseGuards(TokenAuthGuard) + async trailerTranscoding( + @Param('id') id: string, + @Body() body: TranscodingCompletedDTO, + ) { + await this.contentsService.trailerTranscodingCompleted( + id, + body.status, + body.metadata, + ); + } + + @Post(':id/retranscode') + retranscodeContent(@Param('id') id: string) { + return this.contentsService.retranscodeContent(id); + } +} diff --git a/backend/src/contents/contents.module.ts b/backend/src/contents/contents.module.ts new file mode 100644 index 0000000..ed632d4 --- /dev/null +++ b/backend/src/contents/contents.module.ts @@ -0,0 +1,47 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { ContentsController } from './contents.controller'; +import { KeyController } from './key.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Shareholder } from './entities/shareholder.entity'; +import { InvitesModule } from 'src/invites/invites.module'; +import { Cast } from './entities/cast.entity'; +import { Crew } from './entities/crew.entity'; +import { PaymentModule } from 'src/payment/payment.module'; +import { ContentsService } from './contents.service'; +import { Content } from './entities/content.entity'; +import { ContentKey } from './entities/content-key.entity'; +import { ProjectsModule } from 'src/projects/projects.module'; +import { Caption } from './entities/caption.entity'; +import { RssShareholder } from './entities/rss-shareholder.entity'; +import { BullModule } from '@nestjs/bullmq'; +import { Season } from 'src/season/entities/season.entity'; +import { SeasonService } from 'src/season/season.service'; +import { SeasonRent } from 'src/season/entities/season-rents.entity'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Shareholder, + RssShareholder, + Cast, + Crew, + Content, + ContentKey, + Caption, + Season, + SeasonRent, + Trailer, + ]), + InvitesModule, + forwardRef(() => PaymentModule), + forwardRef(() => ProjectsModule), + BullModule.registerQueue({ + name: 'transcode', + }), + ], + controllers: [ContentsController, KeyController], + providers: [ContentsService, SeasonService], + exports: [ContentsService], +}) +export class ContentsModule {} diff --git a/backend/src/contents/contents.service.ts b/backend/src/contents/contents.service.ts new file mode 100644 index 0000000..224f49d --- /dev/null +++ b/backend/src/contents/contents.service.ts @@ -0,0 +1,961 @@ +import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { randomUUID } from 'node:crypto'; +import { Shareholder } from './entities/shareholder.entity'; +import { UploadService } from 'src/upload/upload.service'; +import { InvitesService } from 'src/invites/invites.service'; +import { Cast } from './entities/cast.entity'; +import { MailService } from 'src/mail/mail.service'; +import { Crew } from './entities/crew.entity'; +import { PaymentService } from 'src/payment/services/payment.service'; +import { Payment } from 'src/payment/entities/payment.entity'; +import { Invite } from 'src/invites/entities/invite.entity'; +import { + Content, + defaultRelations, + fullRelations, +} from './entities/content.entity'; +import { UpdateContentDTO } from './dto/request/update-content.dto'; +import { ListContentsDTO } from './dto/request/list-contents.dto'; +import { Caption } from './entities/caption.entity'; +import { + getFileRoute, + getTranscodedFileRoute, + getTrailerTranscodeOutputKey, + getTrailerTranscodedFileRoute, +} from 'src/common/helper'; +import { ContentStatus } from './enums/content-status.enum'; +import { RssShareholder } from './entities/rss-shareholder.entity'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Transcode } from './types/transcode'; +import { Queue } from 'bullmq'; +import { Project } from 'src/projects/entities/project.entity'; +import { SeasonService } from 'src/season/season.service'; +import { UpdateSeasonDto } from 'src/season/dto/request/update-season.dto.entity'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; + +type MappedPayment = { + [key: string]: { usd: number; milisat: number }; +}; + +@Injectable() +export class ContentsService { + constructor( + @InjectRepository(Content) + private contentsRepository: Repository, + @InjectRepository(Shareholder) + private shareholderRepository: Repository, + @InjectRepository(RssShareholder) + private rssShareholderRepository: Repository, + @InjectRepository(Cast) + private castRepository: Repository, + @InjectRepository(Crew) + private crewRepository: Repository, + @InjectRepository(Caption) + private captionRepository: Repository, + @InjectRepository(Trailer) + private trailerRepository: Repository, + @Inject(UploadService) + private uploadService: UploadService, + @Inject(InvitesService) + private invitesService: InvitesService, + @Inject(MailService) + private mailService: MailService, + @Inject(PaymentService) + private paymentsService: PaymentService, + @InjectQueue('transcode') private transcodeQueue: Queue, + private seasonService: SeasonService, + ) {} + + findAll(projectId: string, query: ListContentsDTO) { + return this.contentsRepository.find({ + where: { projectId, season: query.season }, + order: { + order: 'ASC', + }, + skip: query.offset, + take: query.limit, + }); + } + + findOne(id: string, relations: string[] = defaultRelations) { + return this.contentsRepository.findOneOrFail({ + where: { id }, + relations, + }); + } + + async upsert(projectId: string, updateContentDTO: UpdateContentDTO) { + let content: Content; + let shouldQueueTrailer = false; + let removeContentTrailerJobs = false; + const trailerProvided: boolean = Object.prototype.hasOwnProperty.call( + updateContentDTO, + 'trailer', + ); + if (updateContentDTO.id) { + content = await this.findOne(updateContentDTO.id); + const updatePayload: Partial = { + title: updateContentDTO.title, + synopsis: updateContentDTO.synopsis, + file: updateContentDTO.file, + poster: updateContentDTO.poster, + order: updateContentDTO.order, + rentalPrice: updateContentDTO.rentalPrice, + status: updateContentDTO.file ? 'processing' : content.status, + releaseDate: updateContentDTO?.releaseDate + ? new Date(updateContentDTO.releaseDate) + : new Date(), + isRssEnabled: updateContentDTO.isRssEnabled, + }; + + let updatedTrailer: Trailer | null | undefined; + let trailerToRemove: Trailer | undefined; + + if (trailerProvided) { + const newTrailerFile = updateContentDTO.trailer; + const previousTrailerFile = content.trailer?.file; + + if (newTrailerFile) { + const trailerChanged = previousTrailerFile !== newTrailerFile; + + if (trailerChanged && previousTrailerFile) { + await this.deleteTrailerAssets(previousTrailerFile); + } + + const trailerEntity = + content.trailer ?? this.trailerRepository.create(); + const resetTranscoding = + trailerChanged || content.trailer === undefined; + trailerEntity.file = newTrailerFile; + if (resetTranscoding) { + trailerEntity.status = 'processing'; + // eslint-disable-next-line unicorn/no-null -- Ensure trailer metadata resets after new upload + trailerEntity.metadata = null; + } + updatedTrailer = await this.trailerRepository.save(trailerEntity); + shouldQueueTrailer = resetTranscoding; + } else if (content.trailer) { + if (content.trailer.file) { + await this.deleteTrailerAssets(content.trailer.file); + } + trailerToRemove = content.trailer; + // eslint-disable-next-line unicorn/no-null -- Explicitly clear the trailer relation. + updatedTrailer = null; + removeContentTrailerJobs = true; + } else { + updatedTrailer = undefined; + } + + updatePayload.trailer = + updatedTrailer === undefined ? content.trailer : updatedTrailer; + } + + await this.contentsRepository.save({ + ...content, + ...updatePayload, + }); + if (trailerToRemove) { + await this.trailerRepository.remove(trailerToRemove); + } + } else { + let trailerEntity: Trailer | undefined; + if (updateContentDTO.trailer) { + trailerEntity = await this.trailerRepository.save( + this.trailerRepository.create({ + file: updateContentDTO.trailer, + status: 'processing', + // eslint-disable-next-line unicorn/no-null -- Trailer metadata reset on creation + metadata: null, + }), + ); + } + content = await this.contentsRepository.save({ + id: randomUUID(), + projectId, + title: updateContentDTO.title, + synopsis: updateContentDTO.synopsis, + file: updateContentDTO.file, + trailer: trailerEntity, + poster: updateContentDTO.poster, + season: updateContentDTO.season, + scheduledFor: updateContentDTO.scheduledFor, + rentalPrice: updateContentDTO.rentalPrice, + order: updateContentDTO.order, + status: 'processing', + releaseDate: updateContentDTO?.releaseDate + ? new Date(updateContentDTO.releaseDate) + : new Date(), + isRssEnabled: updateContentDTO.isRssEnabled, + }); + shouldQueueTrailer = !!trailerEntity; + // For episodic content, create seasons if they don't exist + if (updateContentDTO.season) { + const existingSeasons = + await this.seasonService.findSeasonsByProjectAndNumbers(projectId, [ + updateContentDTO.season, + ]); + if (existingSeasons.length === 0) { + // Create Season + await this.seasonService.getOrCreateSeasonsUpsert(projectId, [ + { seasonNumber: updateContentDTO.season } as UpdateSeasonDto, + ]); + } + } + } + + if ( + updateContentDTO.file && + updateContentDTO.file !== content.file && + content.file && + content.file !== '' + ) { + await this.uploadService.deleteObject( + content.file, + process.env.S3_PRIVATE_BUCKET_NAME, + ); + await this.uploadService.deleteObject( + getTranscodedFileRoute(content.file), + process.env.S3_PUBLIC_BUCKET_NAME, + ); + } + + if ( + updateContentDTO.poster && + updateContentDTO.poster !== content.poster && + content.poster + ) { + await this.uploadService.deleteObject( + content.poster, + process.env.S3_PUBLIC_BUCKET_NAME, + ); + } + + if (removeContentTrailerJobs) { + await this.removeManyTranscodingJobs([`${content.id}:trailer`]); + } + + const newMembers: Array<{ id?: string; email?: string }> = []; + + if ( + updateContentDTO.shareholders && + updateContentDTO.shareholders.length > 0 + ) { + for (const shareholder of content.shareholders || []) { + const shareholderDTO = updateContentDTO.shareholders.find( + (fm) => fm.id === shareholder.filmmakerId, + ); + await (shareholderDTO + ? this.shareholderRepository.update( + { + id: shareholder.id, + contentId: content.id, + filmmakerId: shareholder.filmmakerId, + }, + { + share: shareholderDTO.share, + }, + ) + : this.shareholderRepository.softDelete({ + id: shareholder.id, + filmmakerId: shareholder.filmmakerId, + contentId: content.id, + })); + } + + for (const shareholderDTO of updateContentDTO.shareholders) { + const shareholder = (content.shareholders || []).find( + (fm) => fm.filmmakerId === shareholderDTO.id, + ); + if (!shareholder) { + await this.shareholderRepository.save({ + id: randomUUID(), + filmmakerId: shareholderDTO.id, + share: shareholderDTO.share, + contentId: content.id, + isOwner: false, + }); + newMembers.push(shareholderDTO); + } + } + } + + if (updateContentDTO.rssShareholders && updateContentDTO.isRssEnabled) { + for (const rssShareholder of content.rssShareholders || []) { + const shareholderDTO = updateContentDTO.rssShareholders.find( + (fm) => fm.id === rssShareholder.id, + ); + await (shareholderDTO + ? this.rssShareholderRepository.update( + { + id: rssShareholder.id, + contentId: content.id, + }, + { + share: shareholderDTO.share, + lightningAddress: shareholderDTO.lightningAddress, + name: shareholderDTO.name, + nodePublicKey: shareholderDTO.nodePublicKey, + key: shareholderDTO.key, + value: shareholderDTO.value, + }, + ) + : this.rssShareholderRepository.delete({ + id: rssShareholder.id, + })); + } + + for (const rssShareholderDTO of updateContentDTO.rssShareholders) { + const shareholder = (content.rssShareholders || []).find( + (rs) => rs.id === rssShareholderDTO.id, + ); + if (!shareholder) { + await this.rssShareholderRepository.save({ + id: randomUUID(), + share: rssShareholderDTO.share, + contentId: content.id, + lightningAddress: rssShareholderDTO.lightningAddress, + name: rssShareholderDTO.name, + nodePublicKey: rssShareholderDTO.nodePublicKey, + key: rssShareholderDTO.key, + value: rssShareholderDTO.value, + }); + } + } + } + + if (!updateContentDTO.isRssEnabled) { + await this.rssShareholderRepository.delete({ + contentId: content.id, + }); + } + + if (updateContentDTO.cast) { + for (const cast of content.cast || []) { + const castDTO = updateContentDTO.cast.find( + (c) => + c.character === cast.character && + ((c.email && c.email === cast.email) || c.id === cast.filmmakerId), + ); + await (castDTO + ? this.castRepository.update( + { + id: cast.id, + contentId: content.id, + }, + { + placeholderName: castDTO.placeholderName, + character: castDTO.character, + order: castDTO.order, + }, + ) + : this.castRepository.delete({ + id: cast.id, + contentId: content.id, + })); + } + + for (const castDTO of updateContentDTO.cast) { + const cast = (content.cast || []).find( + (c) => + c.character === castDTO.character && + (c.filmmakerId === castDTO.id || + (c.email && c.email === castDTO.email)), + ); + if (!cast) { + await this.castRepository.save({ + id: randomUUID(), + placeholderName: castDTO.placeholderName, + character: castDTO.character, + email: castDTO.email, + order: castDTO.order, + contentId: content.id, + filmmakerId: castDTO.id, + }); + + newMembers.push(castDTO); + } + } + } + + if (updateContentDTO.crew) { + for (const crew of content.crew || []) { + const crewDTO = updateContentDTO.crew.find( + (c) => + c.occupation === crew.occupation && + (c.id === crew.filmmakerId || + (crew.email && c.email === crew.email)), + ); + await (crewDTO + ? this.crewRepository.update( + { + id: crew.id, + contentId: content.id, + }, + { + placeholderName: crewDTO.placeholderName, + occupation: crewDTO.occupation, + order: crewDTO.order, + }, + ) + : this.crewRepository.delete({ + id: crew.id, + contentId: content.id, + })); + } + + for (const crewDTO of updateContentDTO.crew) { + const crew = (content.crew || []).find( + (c) => + c.occupation === crewDTO.occupation && + (c.filmmakerId === crewDTO.id || + (c.email && c.email === crewDTO.email)), + ); + if (!crew) { + await this.crewRepository.save({ + id: randomUUID(), + occupation: crewDTO.occupation, + placeholderName: crewDTO.placeholderName, + email: crewDTO.email, + order: crewDTO.order, + contentId: content.id, + filmmakerId: crewDTO.id, + }); + + newMembers.push(crewDTO); + } + } + } + + if (updateContentDTO.invites) { + for (const invite of content.invites || []) { + const inviteDTO = updateContentDTO.invites.find( + (inv) => inv.email === invite.email, + ); + if (!inviteDTO) { + await this.invitesService.removeInvite(invite.email, content.id); + } + } + + const invitesToBeSent: Array<{ email: string; share: number }> = []; + + for (const invite of updateContentDTO.invites) { + const existingInvite = content.invites.find( + (inv) => inv.email === invite.email, + ); + if (existingInvite) { + await this.invitesService.updateInvite( + existingInvite.email, + content.id, + invite.share, + ); + } else { + invitesToBeSent.push({ + email: invite.email, + share: invite.share, + }); + newMembers.push({ email: invite.email }); + } + } + + await this.invitesService.sendInvites(invitesToBeSent, content.id); + } + + if (updateContentDTO.captions) { + for (const caption of content.captions || []) { + const captionDTO = updateContentDTO.captions.find( + (c) => c.id === caption.id, + ); + if (!captionDTO) { + await this.captionRepository.delete({ + id: caption.id, + contentId: content.id, + }); + await this.uploadService.deleteObject( + caption.url, + process.env.S3_PRIVATE_BUCKET_NAME, + ); + } + } + + for (const captionDTO of updateContentDTO.captions) { + const caption = (content.captions || []).find( + (d) => d.id === captionDTO.id, + ); + if (!caption) { + await this.captionRepository.save({ + id: randomUUID(), + language: captionDTO.language, + url: captionDTO.url, + contentId: content.id, + }); + } + } + } + + const updatedContent = await this.findOne(content.id, [ + 'project', + ...fullRelations, + ]); + if ( + updateContentDTO.file && + updatedContent.project?.status === 'published' + ) { + await this.sendToTranscodingQueue(updatedContent, updatedContent.project); + } + + if ( + shouldQueueTrailer && + updatedContent.project?.status === 'published' && + updatedContent.trailer?.file + ) { + await this.sendTrailerToTranscodingQueue( + updatedContent, + updatedContent.project, + ); + } + + return { updatedContent, newMembers }; + } + + async remove(id: string) { + try { + const content = await this.contentsRepository.findOneOrFail({ + where: { id }, + relations: ['trailer'], + }); + if (content.file) { + await this.uploadService.deleteObject( + content.file, + process.env.S3_PRIVATE_BUCKET_NAME, + ); + await this.uploadService.deleteObject( + getTranscodedFileRoute(content.file), + process.env.S3_PUBLIC_BUCKET_NAME, + ); + } + if (content.poster) { + await this.uploadService.deleteObject( + content.poster, + process.env.S3_PUBLIC_BUCKET_NAME, + ); + } + const trailerToRemove = content.trailer; + if (trailerToRemove?.file) { + await this.deleteTrailerAssets(trailerToRemove.file); + } + content.file = undefined; + // eslint-disable-next-line unicorn/no-null -- Remove the FK before deleting the trailer. + content.trailer = null; + await this.contentsRepository.save(content); + if (trailerToRemove) { + await this.trailerRepository.remove(trailerToRemove); + } + const result = await this.contentsRepository.softDelete({ id }); + await this.removeManyTranscodingJobs([id, `${id}:trailer`]); + return { success: result.affected > 0 }; + } catch (error) { + Logger.error(error); + return { success: false }; + } + } + + async removeManyTranscodingJobs(contentIds: string[]) { + const existing = await this.transcodeQueue.getJobs(['waiting', 'active']); + const contentsId = new Set(contentIds); + for (const job of existing) { + const data = job.data; + if (contentsId.has(data.correlationId)) { + await job.remove(); + } + } + } + async removeAll(projectId: string) { + try { + const contents = await this.contentsRepository.find({ + where: { projectId }, + relations: ['trailer'], + }); + await Promise.all( + contents.map(async (content) => { + if (content.file) + await this.uploadService.deleteObject( + content.file, + process.env.S3_PRIVATE_BUCKET_NAME, + ); + if (content.poster) + await this.uploadService.deleteObject( + content.poster, + process.env.S3_PUBLIC_BUCKET_NAME, + ); + const trailerToRemove = content.trailer; + if (trailerToRemove?.file) { + await this.deleteTrailerAssets(trailerToRemove.file); + } + if (trailerToRemove) { + // eslint-disable-next-line unicorn/no-null -- Remove the FK before deleting the trailer. + content.trailer = null; + await this.contentsRepository.save(content); + await this.trailerRepository.remove(trailerToRemove); + } + await this.contentsRepository.softDelete({ id: content.id }); + }), + ); + const jobsToRemove = contents.flatMap((item) => [ + item.id, + `${item.id}:trailer`, + ]); + await this.removeManyTranscodingJobs(jobsToRemove); + return { success: true }; + } catch (error) { + Logger.error(error); + return { success: false }; + } + } + + async stream(id: string) { + const project = await this.findOne(id, ['project', 'captions', 'trailer']); + return project; + } + + async addInvitedFilmmaker(filmmakerId: string, invite: Invite) { + const invited = this.shareholderRepository.create({ + id: randomUUID(), + filmmakerId, + contentId: invite.contentId, + isOwner: false, + share: invite.share, + }); + await this.shareholderRepository.save(invited); + } + + async updateOnRegister(email: string, filmmakerId: string) { + await Promise.all([ + this.castRepository.update( + { email }, + { + filmmakerId, + email: undefined, + }, + ), + this.crewRepository.update( + { email }, + { + filmmakerId, + email: undefined, + }, + ), + ]); + } + + async getProjectsRevenueAnalytics( + ids: string[], + filmmakerId: string, + startDate: Date, + ) { + const satPricePromise = this.paymentsService.getSatPrice(); + + const userShareholders = await this.shareholderRepository.find({ + where: { + content: { projectId: In(ids) }, + filmmakerId, + }, + }); + + const shareholderIds = userShareholders.map((s) => s.id); + + const totalShare = userShareholders.reduce((accumulator, shareholder) => { + return accumulator + Number(shareholder.share); + }, 0); + + const { usd: userTotalUsd, milisat: userTotalMilisat } = + await this.getShareholdersTotal(shareholderIds, startDate); + + const { usd: totalUsd, milisat: totalMilisat } = await this.getTotalRevenue( + ids, + startDate, + ); + + const satPrice = await satPricePromise; + const userBalance = userShareholders.reduce((accumulator, shareholder) => { + return ( + accumulator + + Number(shareholder.rentPendingRevenue) + + Number(shareholder.pendingRevenue) + ); + }, 0); + + return { + total: { + usd: totalUsd, + milisat: totalMilisat, + }, + user: { + averageShare: + Math.round((totalShare / userShareholders.length) * 100) / 100, + usd: userTotalUsd, + milisat: userTotalMilisat, + balance: { + usd: userBalance, + milisat: Number((userBalance / satPrice) * 1000), + }, + }, + }; + } + + async getShareholdersTotal(shareholderIds: string[], startDate?: Date) { + const payments = await this.paymentsService.getPaymentsByDate( + shareholderIds, + startDate, + ); + + const usd = payments.reduce((accumulator, payment) => { + return accumulator + Number(payment.usdAmount); + }, 0); + const milisat = payments.reduce((accumulator, payment) => { + return accumulator + Number(payment.milisatsAmount); + }, 0); + + return { payments, usd, milisat }; + } + + getRevenueByDate(payments: Payment[]): MappedPayment { + const mappedPayments: MappedPayment = {}; + for (const payment of payments) { + const date = payment.createdAt.toISOString().split('T')[0]; + if (!mappedPayments[date]) { + mappedPayments[date] = { usd: 0, milisat: 0 }; + } + mappedPayments[date].usd += Number(payment.usdAmount); + mappedPayments[date].milisat += Number(payment.milisatsAmount); + } + return mappedPayments; + } + + async getTotalRevenue(projectId: string[], startDate?: Date) { + const shareholders = await this.shareholderRepository.find({ + where: { content: { projectId: In(projectId) } }, + }); + + const payments = await this.paymentsService.getPaymentsByDate( + shareholders.map((s) => s.id), + startDate, + ); + + const usd = payments.reduce((accumulator, payment) => { + return accumulator + Number(payment.usdAmount); + }, 0); + const milisat = payments.reduce((accumulator, payment) => { + return accumulator + Number(payment.milisatsAmount); + }, 0); + + return { usd, milisat }; + } + + getCastFilterSubquery(filmmakerId: string) { + return this.castRepository + .createQueryBuilder('cast') + .leftJoin('cast.content', 'content') + .select('content.projectId') + .where('cast.filmmakerId = :filmmakerId', { + filmmakerId, + }); + } + + getCrewFilterSubquery(filmmakerId: string) { + return this.crewRepository + .createQueryBuilder('crew') + .leftJoin('crew.content', 'content') + .select('content.projectId') + .where('crew.filmmakerId = :filmmakerId', { + filmmakerId, + }); + } + + getShareholderFilterSubquery(filmmakerId: string) { + return this.shareholderRepository + .createQueryBuilder('shareholders') + .leftJoin('shareholders.content', 'content') + .select('content.projectId') + .where('shareholders.filmmakerId = :filmmakerId', { + filmmakerId, + }); + } + + getCompletedProjectsSubquery() { + return this.contentsRepository + .createQueryBuilder('content') + .select('content.projectId') + .where('content.status = :content_status', { + content_status: 'completed', + }) + .groupBy('content.projectId'); + } + + async sendToTranscodingQueue(content: Content, project: Project) { + await this.removeManyTranscodingJobs([content.id]); + Logger.log(`Sending content ${content.id} to transcoding queue`); + const drmContentId = randomUUID().replaceAll('-', ''); + const drmMediaId = randomUUID().replaceAll('-', ''); + + await this.contentsRepository.update(content.id, { + drmContentId, + drmMediaId, + }); + + const outputKey = + project.type === 'episodic' + ? `${getFileRoute(content.file)}${content.id}/transcoded` + : `${getFileRoute(content.file)}transcoded`; + + if (!content.isRssEnabled) { + return await this.transcodeQueue.add('transcode-with-drm', { + inputBucket: process.env.S3_PRIVATE_BUCKET_NAME, + outputBucket: process.env.S3_PUBLIC_BUCKET_NAME, + inputKey: content.file, + outputKey, + correlationId: content.id, + callbackUrl: `${process.env.ENVIRONMENT === 'development' || process.env.ENVIRONMENT === 'local' ? 'http' : 'https'}://${process.env.DOMAIN}/contents/${content.id}/transcoding`, + drmContentId, + drmMediaId, + }); + } + + await this.transcodeQueue.add('transcode', { + inputBucket: process.env.S3_PRIVATE_BUCKET_NAME, + outputBucket: process.env.S3_PUBLIC_BUCKET_NAME, + inputKey: content.file, + outputKey, + correlationId: content.id, + callbackUrl: `https://${process.env.DOMAIN}/contents/${content.id}/transcoding`, + drmContentId, + drmMediaId, + }); + } + + async sendTrailerToTranscodingQueue(content: Content, _project: Project) { + if (!content.trailer?.file) return; + await this.removeManyTranscodingJobs([`${content.id}:trailer`]); + Logger.log(`Sending trailer ${content.id} to transcoding queue`); + + await this.trailerRepository.update(content.trailer.id, { + status: 'processing', + metadata: undefined, + }); + + const callbackProtocol = + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ? 'http' + : 'https'; + + await this.transcodeQueue.add('transcode', { + inputBucket: process.env.S3_PRIVATE_BUCKET_NAME, + outputBucket: process.env.S3_PUBLIC_BUCKET_NAME, + inputKey: content.trailer.file, + outputKey: getTrailerTranscodeOutputKey(content.trailer.file), + correlationId: `${content.id}:trailer`, + callbackUrl: `${callbackProtocol}://${process.env.DOMAIN}/contents/${content.id}/trailer/transcoding`, + }); + } + + async transcodingCompleted(id: string, status: ContentStatus, metadata: any) { + Logger.log(`Transcoding completed for content ${id}, status: ${status}`); + const result = await this.contentsRepository.update( + { id }, + { + status, + metadata, + updatedAt: () => 'updated_at', + }, + ); + const content = await this.findOne(id, [ + 'project', + 'project.permissions', + 'project.permissions.filmmaker', + 'project.permissions.filmmaker.user', + ]); + const ownerEmail = content.project.permissions.find( + (p) => p.role === 'owner', + ).filmmaker.user.email; + const url = `${process.env.FRONTEND_URL}/project/${content.projectId}/details`; + if (status === 'completed') { + this.mailService.sendMail({ + to: ownerEmail, + templateId: 'd-ca1940fba8274abd972338a4bc77b08e', + data: { + filmName: content.title, + url, + }, + }); + } + return result.affected > 0; + } + + async trailerTranscodingCompleted( + id: string, + status: ContentStatus, + metadata: any, + ) { + Logger.log( + `Trailer transcoding completed for content ${id}, status: ${status}`, + ); + const content = await this.contentsRepository.findOne({ + where: { id }, + relations: ['trailer'], + }); + if (!content?.trailer) return false; + await this.trailerRepository.update(content.trailer.id, { + status, + metadata, + }); + return true; + } + + private async deleteTrailerAssets(trailerFile: string) { + await Promise.all([ + this.uploadService.deleteObject( + trailerFile, + process.env.S3_PRIVATE_BUCKET_NAME, + ), + this.uploadService.deleteObject( + trailerFile, + process.env.S3_PUBLIC_BUCKET_NAME, + ), + this.uploadService.deleteObject( + getTrailerTranscodedFileRoute(trailerFile), + process.env.S3_PRIVATE_BUCKET_NAME, + ), + this.uploadService.deleteObject( + getTrailerTranscodedFileRoute(trailerFile), + process.env.S3_PUBLIC_BUCKET_NAME, + ), + ]); + } + + async removeFilmmaker(filmmakerId: string) { + await this.shareholderRepository.delete({ + filmmakerId, + }); + await this.castRepository.delete({ + filmmakerId, + }); + await this.crewRepository.delete({ + filmmakerId, + }); + return { success: true }; + } + + async retranscodeContent(id: string) { + try { + const content = await this.findOne(id, ['project']); + content.status = 'processing'; + await this.contentsRepository.save(content); + await this.sendToTranscodingQueue(content, content.project); + return { success: true }; + } catch { + throw new NotFoundException(); + } + } +} diff --git a/backend/src/contents/dto/request/add-content.dto.ts b/backend/src/contents/dto/request/add-content.dto.ts new file mode 100644 index 0000000..3a09735 --- /dev/null +++ b/backend/src/contents/dto/request/add-content.dto.ts @@ -0,0 +1,223 @@ +import { + IsBoolean, + IsDate, + IsDateString, + IsEmail, + IsEnum, + IsNumber, + IsOptional, + IsString, + Max, + MaxLength, + Min, + ValidateNested, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { Language, languages } from 'src/contents/enums/language.enum'; + +export class AddContentDTO { + @IsString() + @MaxLength(100) + title?: string; + + @IsString() + @MaxLength(350) + @IsOptional() + synopsis?: string; + + @IsString() + @IsOptional() + file?: string; + + @IsString() + @IsOptional() + trailer?: string; + + @IsString() + @IsOptional() + poster?: string; + + @IsNumber() + @IsOptional() + season?: number; + + @IsNumber() + @IsOptional() + order?: number; + + @IsDate() + @IsOptional() + scheduledFor?: Date; + + @IsDateString() + @IsOptional() + releaseDate?: Date; + + @IsNumber() + @IsOptional() + rentalPrice?: number; + + @IsBoolean() + @IsOptional() + isRssEnabled?: boolean; + + @ValidateNested() + @ApiProperty({ type: () => AddCaptionDTO }) + @Type(() => AddCaptionDTO) + captions: AddCaptionDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddShareholderDTO }) + @Type(() => AddShareholderDTO) + shareholders: AddShareholderDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddRssShareholderDTO }) + @Type(() => AddRssShareholderDTO) + rssShareholders: AddRssShareholderDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddCastDTO }) + @Type(() => AddCastDTO) + @IsOptional() + cast?: AddCastDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddCrewDTO }) + @Type(() => AddCrewDTO) + @IsOptional() + crew?: AddCrewDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddShareholderInviteDTO }) + @Type(() => AddShareholderInviteDTO) + @IsOptional() + invites?: AddShareholderInviteDTO[]; +} + +class AddShareholderInviteDTO { + @ApiProperty() + @IsString() + email: string; + + @ApiProperty() + @Max(100) + @Min(0) + share: number; +} + +class AddRssShareholderDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @Max(100) + @Min(0) + share: number; + + @ApiProperty() + @IsOptional() + @IsString() + lightningAddress?: string; + + @ApiProperty() + @IsOptional() + @IsString() + name?: string; + + @ApiProperty() + @IsString() + @IsOptional() + nodePublicKey?: string; + + @ApiProperty() + @IsString() + @IsOptional() + key?: string; + + @ApiProperty() + @IsString() + @IsOptional() + value?: string; +} +class AddShareholderDTO { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @Max(100) + @Min(0) + share: number; +} + +class AddCaptionDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsEnum(languages, { + message: `language must be one of the following: ${languages.join(', ')}`, + }) + language: Language; + + @ApiProperty() + @IsString() + @IsOptional() + url?: string; +} + +class AddCastDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsString() + @IsOptional() + placeholderName?: string; + + @ApiProperty() + @IsString() + character: string; + + @ApiProperty() + @IsEmail() + @IsOptional() + email?: string; + + @ApiProperty() + @IsNumber() + order: number; +} + +class AddCrewDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsString() + @IsOptional() + placeholderName?: string; + + @ApiProperty() + @IsString() + occupation: string; + + @ApiProperty() + @IsEmail() + @IsOptional() + email?: string; + + @ApiProperty() + @IsNumber() + order: number; +} diff --git a/backend/src/contents/dto/request/list-contents.dto.ts b/backend/src/contents/dto/request/list-contents.dto.ts new file mode 100644 index 0000000..d527422 --- /dev/null +++ b/backend/src/contents/dto/request/list-contents.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsOptional } from 'class-validator'; + +export class ListContentsDTO { + @ApiProperty() + @IsOptional() + @Type(() => Number) + season?: number; + + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; +} diff --git a/backend/src/contents/dto/request/transcoding-completed.dto.ts b/backend/src/contents/dto/request/transcoding-completed.dto.ts new file mode 100644 index 0000000..8a47b00 --- /dev/null +++ b/backend/src/contents/dto/request/transcoding-completed.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { + ContentStatus, + contentStatuses, +} from 'src/contents/enums/content-status.enum'; + +export class TranscodingCompletedDTO { + @ApiProperty() + @IsEnum(contentStatuses) + status: ContentStatus; + + @ApiProperty() + @IsOptional() + metadata: any; +} diff --git a/backend/src/contents/dto/request/update-content.dto.ts b/backend/src/contents/dto/request/update-content.dto.ts new file mode 100644 index 0000000..84d3833 --- /dev/null +++ b/backend/src/contents/dto/request/update-content.dto.ts @@ -0,0 +1,10 @@ +import { IsOptional, IsUUID } from 'class-validator'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { AddContentDTO } from './add-content.dto'; + +export class UpdateContentDTO extends PartialType(AddContentDTO) { + @ApiProperty() + @IsUUID() + @IsOptional() + id?: string; +} diff --git a/backend/src/contents/dto/response/base-content.dto.ts b/backend/src/contents/dto/response/base-content.dto.ts new file mode 100644 index 0000000..2e7d7cc --- /dev/null +++ b/backend/src/contents/dto/response/base-content.dto.ts @@ -0,0 +1,82 @@ +import { + getPublicS3Url, + getTrailerTranscodedFileRoute, +} from 'src/common/helper'; +import { CaptionDTO } from './caption.dto'; +import { CastDTO } from './cast.dto'; +import { CrewDTO } from './crew.dto'; +import { Content } from 'src/contents/entities/content.entity'; +import { ContentStatus } from 'src/contents/enums/content-status.enum'; + +export class BaseContentDTO { + id: string; + title: string; + synopsis?: string; + scheduledFor?: Date; + crew: CrewDTO[]; + cast: CastDTO[]; + captions: CaptionDTO[]; + file?: string; + order: number; + season: number | null; + createdAt: Date; + updatedAt: Date; + projectId: string; + rentalPrice: number; + duration?: number; + isRssEnabled: boolean; + releaseDate: Date; + trailer?: string; + poster?: string; + trailerStatus?: ContentStatus; + + constructor(content: Content) { + this.id = content.id; + this.title = content.title; + this.projectId = content.projectId; + this.synopsis = content.synopsis; + this.order = content.order; + this.season = content.season; + this.rentalPrice = content.rentalPrice; + this.isRssEnabled = content.isRssEnabled; + this.releaseDate = content.releaseDate; + + if (content.cast) { + this.cast = content.cast.map((cast) => new CastDTO(cast)); + this.cast.sort((a, b) => a.order - b.order); + } else this.cast = []; + + if (content.crew) { + this.crew = content.crew.map((crew) => new CrewDTO(crew)); + this.crew.sort((a, b) => a.order - b.order); + } else this.crew = []; + + this.captions = content.captions + ? content.captions.map((caption) => new CaptionDTO(caption)) + : []; + + this.createdAt = content.createdAt; + this.updatedAt = content.updatedAt; + + if (content.file) { + this.file = content.file; + } + + this.trailerStatus = content.trailer?.status; + if (content.trailer && this.trailerStatus === 'completed') { + this.trailer = getPublicS3Url( + getTrailerTranscodedFileRoute(content.trailer.file), + ); + } else if (content.trailer) { + this.trailer = content.trailer.file; + } + + if (content.metadata) { + this.duration = content.metadata.format?.duration; + } + + if (content.poster) { + this.poster = getPublicS3Url(content.poster); + } + } +} diff --git a/backend/src/contents/dto/response/caption.dto.ts b/backend/src/contents/dto/response/caption.dto.ts new file mode 100644 index 0000000..c9fd3c6 --- /dev/null +++ b/backend/src/contents/dto/response/caption.dto.ts @@ -0,0 +1,16 @@ +import { Caption } from 'src/contents/entities/caption.entity'; +import { Language } from 'src/contents/enums/language.enum'; + +export class CaptionDTO { + id: string; + language: Language; + url: string; + name: string; + + constructor(caption: Caption) { + this.id = caption.id; + this.name = caption.url.split('/').pop(); + this.language = caption.language; + this.url = process.env.S3_PUBLIC_BUCKET_URL + caption.url; + } +} diff --git a/backend/src/contents/dto/response/cast.dto.ts b/backend/src/contents/dto/response/cast.dto.ts new file mode 100644 index 0000000..9c4c2be --- /dev/null +++ b/backend/src/contents/dto/response/cast.dto.ts @@ -0,0 +1,30 @@ +import { FilmmakerDTO } from 'src/filmmakers/dto/response/filmmaker.dto'; +import { Cast } from 'src/contents/entities/cast.entity'; + +export class CastDTO extends FilmmakerDTO { + character: string; + order: number; + email: string; + placeholderName: string; + + constructor(cast: Cast) { + super(cast.filmmaker); + this.placeholderName = cast.placeholderName; + this.character = cast.character; + this.order = cast.order; + this.email = cast.email; + } +} + +export class PublicCastDTO extends FilmmakerDTO { + placeholderName: string; + character: string; + order: number; + + constructor(cast: Cast) { + super(cast.filmmaker); + this.placeholderName = cast.placeholderName; + this.character = cast.character; + this.order = cast.order; + } +} diff --git a/backend/src/contents/dto/response/content.dto.ts b/backend/src/contents/dto/response/content.dto.ts new file mode 100644 index 0000000..d3f6572 --- /dev/null +++ b/backend/src/contents/dto/response/content.dto.ts @@ -0,0 +1,51 @@ +import { ShareholderDTO } from './shareholder.dto'; +import { InviteShareholderDTO } from './invite.dto'; +import { Content } from 'src/contents/entities/content.entity'; +import { BaseContentDTO } from './base-content.dto'; +import { ContentStatus } from 'src/contents/enums/content-status.enum'; +import { RssShareholderDTO } from './rss-shareholder.dto'; +import { CaptionDTO } from './caption.dto'; + +export class ContentDTO extends BaseContentDTO { + invites: InviteShareholderDTO[]; + shareholders: ShareholderDTO[]; + isRssEnabled: boolean; + rssShareholders: RssShareholderDTO[]; + status: ContentStatus; + + constructor(content: Content) { + super(content); + this.status = content.status; + this.isRssEnabled = content.isRssEnabled; + + if (content.shareholders) { + this.shareholders = content.shareholders.map( + (shareholder) => new ShareholderDTO(shareholder), + ); + this.shareholders.sort((a, b) => { + if (a.isOwner && !b.isOwner) { + return -1; + } + if (!a.isOwner && b.isOwner) { + return 1; + } + return b.share - a.share; + }); + } else this.shareholders = []; + + this.invites = content.invites + ? content.invites.map((invite) => new InviteShareholderDTO(invite)) + : []; + + this.rssShareholders = + content.rssShareholders && content.isRssEnabled + ? content.rssShareholders.map( + (rssShareholder) => new RssShareholderDTO(rssShareholder), + ) + : []; + + this.captions = content?.captions + ? content.captions.map((caption) => new CaptionDTO(caption)) + : []; + } +} diff --git a/backend/src/contents/dto/response/crew.dto.ts b/backend/src/contents/dto/response/crew.dto.ts new file mode 100644 index 0000000..07ce2f0 --- /dev/null +++ b/backend/src/contents/dto/response/crew.dto.ts @@ -0,0 +1,30 @@ +import { FilmmakerDTO } from 'src/filmmakers/dto/response/filmmaker.dto'; +import { Crew } from 'src/contents/entities/crew.entity'; + +export class PublicCrewDTO extends FilmmakerDTO { + placeholderName: string; + occupation: string; + order: number; + + constructor(cast: Crew) { + super(cast.filmmaker); + this.placeholderName = cast.placeholderName; + this.occupation = cast.occupation; + this.order = cast.order; + } +} + +export class CrewDTO extends FilmmakerDTO { + occupation: string; + order: number; + email: string; + placeholderName: string; + + constructor(cast: Crew) { + super(cast.filmmaker); + this.placeholderName = cast.placeholderName; + this.occupation = cast.occupation; + this.order = cast.order; + this.email = cast.email; + } +} diff --git a/backend/src/contents/dto/response/invite.dto.ts b/backend/src/contents/dto/response/invite.dto.ts new file mode 100644 index 0000000..fba53a1 --- /dev/null +++ b/backend/src/contents/dto/response/invite.dto.ts @@ -0,0 +1,11 @@ +import { Invite } from 'src/invites/entities/invite.entity'; + +export class InviteShareholderDTO { + email: string; + share: number; + + constructor(invite: Invite) { + this.email = invite.email; + this.share = invite.share; + } +} diff --git a/backend/src/contents/dto/response/rss-shareholder.dto.ts b/backend/src/contents/dto/response/rss-shareholder.dto.ts new file mode 100644 index 0000000..5332b79 --- /dev/null +++ b/backend/src/contents/dto/response/rss-shareholder.dto.ts @@ -0,0 +1,21 @@ +import { RssShareholder } from 'src/contents/entities/rss-shareholder.entity'; + +export class RssShareholderDTO { + id: string; + share: number; + lightningAddress?: string; + name?: string; + nodePublicKey?: string; + key?: string; + value?: string; + + constructor(shareholder: RssShareholder) { + this.id = shareholder.id; + this.share = shareholder.share; + this.lightningAddress = shareholder.lightningAddress; + this.name = shareholder.name; + this.nodePublicKey = shareholder.nodePublicKey; + this.key = shareholder.key; + this.value = shareholder.value; + } +} diff --git a/backend/src/contents/dto/response/shareholder.dto.ts b/backend/src/contents/dto/response/shareholder.dto.ts new file mode 100644 index 0000000..dc34cf6 --- /dev/null +++ b/backend/src/contents/dto/response/shareholder.dto.ts @@ -0,0 +1,14 @@ +import { FilmmakerDTO } from 'src/filmmakers/dto/response/filmmaker.dto'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; + +export class ShareholderDTO extends FilmmakerDTO { + role: string; + share: number; + isOwner: boolean; + + constructor(shareholder: Shareholder) { + super(shareholder.filmmaker); + this.share = shareholder.share; + this.isOwner = shareholder.isOwner; + } +} diff --git a/backend/src/contents/dto/response/stream-content.dto.ts b/backend/src/contents/dto/response/stream-content.dto.ts new file mode 100644 index 0000000..0511f8d --- /dev/null +++ b/backend/src/contents/dto/response/stream-content.dto.ts @@ -0,0 +1,21 @@ +import { Content } from 'src/contents/entities/content.entity'; +import { BaseContentDTO } from './base-content.dto'; +import { getFileRoute, getPublicS3Url } from 'src/common/helper'; + +export class StreamContentDTO extends BaseContentDTO { + file: string; + widevine: string; + fairplay: string; + constructor(content: Content) { + super(content); + const outputKey = + content.project.type === 'episodic' + ? `${getFileRoute(content.file)}${content.id}/` + : `${getFileRoute(content.file)}`; + const outputUrl = getPublicS3Url(outputKey); + + this.file = `${outputUrl}transcoded/file.m3u8`; + this.widevine = `${outputUrl}transcoded/encrypted/video_master.mpd`; + this.fairplay = `${outputUrl}transcoded/encrypted/video_master.m3u8`; + } +} diff --git a/backend/src/contents/entities/caption.entity.ts b/backend/src/contents/entities/caption.entity.ts new file mode 100644 index 0000000..791cecc --- /dev/null +++ b/backend/src/contents/entities/caption.entity.ts @@ -0,0 +1,36 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Content } from './content.entity'; +import { Language } from 'src/contents/enums/language.enum'; + +@Entity('captions') +export class Caption { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + contentId: string; + + @Column() + language: Language; + + @Column() + url: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Content, (content) => content.crew) + @JoinColumn({ name: 'content_id' }) + content: Content; +} diff --git a/backend/src/contents/entities/cast.entity.ts b/backend/src/contents/entities/cast.entity.ts new file mode 100644 index 0000000..f45e11e --- /dev/null +++ b/backend/src/contents/entities/cast.entity.ts @@ -0,0 +1,49 @@ +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Content } from './content.entity'; + +@Entity('casts') +export class Cast { + @PrimaryColumn() + id: string; + + @Column({ nullable: true }) + filmmakerId?: string; + + @PrimaryColumn() + contentId: string; + + @Column({ nullable: true }) + placeholderName: string; + + @Column() + character: string; + + @Column() + order: number; + + @Column({ nullable: true }) + email: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Filmmaker, (filmmaker) => filmmaker.castFilms) + @JoinColumn({ name: 'filmmaker_id' }) + filmmaker: Filmmaker; + + @ManyToOne(() => Content, (content) => content.cast) + @JoinColumn({ name: 'content_id' }) + content: Content; +} diff --git a/backend/src/contents/entities/content-key.entity.ts b/backend/src/contents/entities/content-key.entity.ts new file mode 100644 index 0000000..14df9a9 --- /dev/null +++ b/backend/src/contents/entities/content-key.entity.ts @@ -0,0 +1,32 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryColumn, +} from 'typeorm'; + +/** + * AES-128 decryption keys for HLS content protection. + * Each content can have multiple keys (for key rotation). + * The raw 16-byte key is stored encrypted at rest (via DB-level encryption). + */ +@Entity('content_keys') +export class ContentKey { + @PrimaryColumn() + id: string; + + @Column() + contentId: string; + + @Column({ type: 'bytea' }) + keyData: Buffer; + + @Column({ type: 'bytea', nullable: true }) + iv: Buffer; + + @Column({ default: 0 }) + rotationIndex: number; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; +} diff --git a/backend/src/contents/entities/content.entity.ts b/backend/src/contents/entities/content.entity.ts new file mode 100644 index 0000000..f994d84 --- /dev/null +++ b/backend/src/contents/entities/content.entity.ts @@ -0,0 +1,164 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Shareholder } from './shareholder.entity'; +import { Invite } from 'src/invites/entities/invite.entity'; +import { Cast } from './cast.entity'; +import { Crew } from './crew.entity'; +import { Rent } from 'src/rents/entities/rent.entity'; +import { Project } from 'src/projects/entities/project.entity'; +import { Caption } from './caption.entity'; +import { ContentStatus, contentStatuses } from '../enums/content-status.enum'; +import { RssShareholder } from './rss-shareholder.entity'; +import { Season } from 'src/season/entities/season.entity'; +import { Discount } from 'src/discounts/entities/discount.entity'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; + +@Entity('contents') +export class Content { + @PrimaryColumn() + id: string; + + @Column() + projectId: string; + + @Column({ nullable: true }) + season_id: string; + + @Column({ nullable: true }) + title: string; + + @Column({ nullable: true }) + synopsis: string; + + @Column({ nullable: true }) + file: string; + + @Column({ nullable: true }) + season?: number; + + @Column({ default: 1 }) + order: number; + + @Column('decimal', { + precision: 10, + scale: 2, + default: 0, + transformer: { + to: (value) => value, + from: (value) => Number.parseFloat(value), + }, + }) + rentalPrice: number; + + @Column({ + enum: contentStatuses, + default: 'processing', + }) + status: ContentStatus; + + @Column('json', { nullable: true }) + metadata: any; + + @Column('boolean', { default: false }) + isRssEnabled: boolean; + + @Column({ nullable: true }) + drmContentId: string; + + @Column({ nullable: true }) + drmMediaId: string; + + @Column({ type: 'timestamptz', default: () => 'NOW()' }) + releaseDate: Date; + + @Column({ nullable: true }) + trailer_old: string; + + @ManyToOne(() => Trailer, (trailer) => trailer.contents, { + nullable: true, + cascade: true, + }) + @JoinColumn({ name: 'trailer_id' }) + trailer?: Trailer; + + @Column({ nullable: true }) + poster: string; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @OneToMany(() => Cast, (cast) => cast.content) + cast: Cast[]; + + @OneToMany(() => Crew, (crew) => crew.content) + crew: Crew[]; + + @OneToMany(() => Shareholder, (shareholder) => shareholder.content) + shareholders: Shareholder[]; + + @OneToMany(() => RssShareholder, (rssShareholders) => rssShareholders.content) + rssShareholders?: RssShareholder[]; + + @OneToMany(() => Invite, (invite) => invite.content) + invites: Invite[]; + + @OneToMany(() => Rent, (rent) => rent.content) + rents: Rent[]; + + @OneToMany(() => Caption, (caption) => caption.content) + captions: Caption[]; + + @ManyToOne(() => Project, (project) => project.contents) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @ManyToOne(() => Season, (season) => season.contents) + @JoinColumn({ name: 'season_id' }) + seriesSeason: Season; + + @OneToMany(() => Discount, (discount) => discount.content) + discounts: Discount[]; +} + +export const defaultRelations = [ + 'invites', + 'shareholders', + 'rssShareholders', + 'crew', + 'cast', + 'captions', + 'trailer', +]; + +export const fullRelations = [ + 'captions', + 'invites', + 'shareholders', + 'shareholders.filmmaker', + 'shareholders.filmmaker.user', + 'crew', + 'crew.filmmaker', + 'crew.filmmaker.user', + 'cast', + 'cast.filmmaker', + 'cast.filmmaker.user', + 'rssShareholders', + 'project', + 'seriesSeason', + 'trailer', +]; diff --git a/backend/src/contents/entities/crew.entity.ts b/backend/src/contents/entities/crew.entity.ts new file mode 100644 index 0000000..e6ac837 --- /dev/null +++ b/backend/src/contents/entities/crew.entity.ts @@ -0,0 +1,53 @@ +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Content } from './content.entity'; + +@Entity('crews') +export class Crew { + @PrimaryColumn() + id: string; + + @Column({ nullable: true }) + filmmakerId?: string; + + @PrimaryColumn() + contentId: string; + + @Column({ nullable: true }) + placeholderName: string; + + @Column() + occupation: string; + + @Column() + order: number; + + @Column({ nullable: true }) + email: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @ManyToOne(() => Filmmaker, (filmmaker) => filmmaker.crewFilms) + @JoinColumn({ name: 'filmmaker_id' }) + filmmaker: Filmmaker; + + @ManyToOne(() => Content, (content) => content.crew) + @JoinColumn({ name: 'content_id' }) + content: Content; +} diff --git a/backend/src/contents/entities/rss-shareholder.entity.ts b/backend/src/contents/entities/rss-shareholder.entity.ts new file mode 100644 index 0000000..d33e295 --- /dev/null +++ b/backend/src/contents/entities/rss-shareholder.entity.ts @@ -0,0 +1,59 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Content } from './content.entity'; + +@Entity('rss_shareholders') +export class RssShareholder { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + contentId: string; + + @Column('decimal', { + precision: 10, + scale: 2, + default: 0, + transformer: { + to: (value) => value, + from: (value) => Number.parseFloat(value), + }, + }) + share: number; + + @Column({ nullable: true }) + name?: string; + + @Column({ nullable: true }) + lightningAddress?: string; + + @Column({ nullable: true }) + nodePublicKey?: string; + + @Column({ nullable: true }) + key?: string; + + @Column({ nullable: true }) + value?: string; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @ManyToOne(() => Content, (content) => content.rssShareholders) + @JoinColumn({ name: 'content_id' }) + content: Content; +} diff --git a/backend/src/contents/entities/shareholder.entity.ts b/backend/src/contents/entities/shareholder.entity.ts new file mode 100644 index 0000000..573d167 --- /dev/null +++ b/backend/src/contents/entities/shareholder.entity.ts @@ -0,0 +1,74 @@ +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; +import { Payment } from 'src/payment/entities/payment.entity'; +import { Content } from './content.entity'; +import { ColumnNumericTransformer } from 'src/database/transformers/column-numeric-transformer'; + +@Entity('shareholders') +export class Shareholder { + @PrimaryColumn() + @Unique('UQ_shareholder_filmmaker_content', ['filmmakerId', 'contentId']) + id: string; + + @PrimaryColumn() + filmmakerId: string; + + @PrimaryColumn() + contentId: string; + + @Column() + share: number; + + @Column() + isOwner: boolean; + + @Column({ + type: 'decimal', + precision: 20, + scale: 10, + default: 0, + transformer: new ColumnNumericTransformer(), + }) + pendingRevenue: number; + + @Column({ + type: 'decimal', + precision: 20, + scale: 10, + default: 0, + transformer: new ColumnNumericTransformer(), + }) + rentPendingRevenue: number; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @ManyToOne(() => Filmmaker, (filmmaker) => filmmaker.shareholderFilms) + @JoinColumn({ name: 'filmmaker_id' }) + filmmaker?: Filmmaker; + + @ManyToOne(() => Content, (content) => content.shareholders) + @JoinColumn({ name: 'content_id' }) + content: Content; + + @OneToMany(() => Payment, (payment) => payment.shareholder) + @JoinColumn({ name: 'id' }) + payments?: Payment[]; +} diff --git a/backend/src/contents/enums/content-status.enum.ts b/backend/src/contents/enums/content-status.enum.ts new file mode 100644 index 0000000..160d1b6 --- /dev/null +++ b/backend/src/contents/enums/content-status.enum.ts @@ -0,0 +1,2 @@ +export const contentStatuses = ['processing', 'failed', 'completed'] as const; +export type ContentStatus = (typeof contentStatuses)[number]; diff --git a/backend/src/contents/enums/language.enum.ts b/backend/src/contents/enums/language.enum.ts new file mode 100644 index 0000000..b64c741 --- /dev/null +++ b/backend/src/contents/enums/language.enum.ts @@ -0,0 +1,188 @@ +export const languages = [ + 'aa', + 'ab', + 'ae', + 'af', + 'ak', + 'am', + 'an', + 'ar', + 'as', + 'av', + 'ay', + 'az', + 'ba', + 'be', + 'bg', + 'bh', + 'bi', + 'bm', + 'bn', + 'bo', + 'br', + 'bs', + 'ca', + 'ce', + 'ch', + 'co', + 'cr', + 'cs', + 'cu', + 'cv', + 'cy', + 'da', + 'de', + 'dv', + 'dz', + 'ee', + 'el', + 'en', + 'eo', + 'es', + 'et', + 'eu', + 'fa', + 'ff', + 'fi', + 'fj', + 'fo', + 'fr', + 'fy', + 'ga', + 'gd', + 'gl', + 'gn', + 'gu', + 'gv', + 'ha', + 'he', + 'hi', + 'ho', + 'hr', + 'ht', + 'hu', + 'hy', + 'hz', + 'ia', + 'id', + 'ie', + 'ig', + 'ii', + 'ik', + 'io', + 'is', + 'it', + 'iu', + 'ja', + 'jv', + 'ka', + 'kg', + 'ki', + 'kj', + 'kk', + 'kl', + 'km', + 'kn', + 'ko', + 'kr', + 'ks', + 'ku', + 'kv', + 'kw', + 'ky', + 'la', + 'lb', + 'lg', + 'li', + 'ln', + 'lo', + 'lt', + 'lu', + 'lv', + 'mg', + 'mh', + 'mi', + 'mk', + 'ml', + 'mn', + 'mr', + 'ms', + 'mt', + 'my', + 'na', + 'nb', + 'nd', + 'ne', + 'ng', + 'nl', + 'nn', + 'no', + 'nr', + 'nv', + 'ny', + 'oc', + 'oj', + 'om', + 'or', + 'os', + 'pa', + 'pi', + 'pl', + 'ps', + 'pt', + 'qu', + 'rm', + 'rn', + 'ro', + 'ru', + 'rw', + 'sa', + 'sc', + 'sd', + 'se', + 'sg', + 'si', + 'sk', + 'sl', + 'sm', + 'sn', + 'so', + 'sq', + 'sr', + 'ss', + 'st', + 'su', + 'sv', + 'sw', + 'ta', + 'te', + 'tg', + 'th', + 'ti', + 'tk', + 'tl', + 'tn', + 'to', + 'tr', + 'ts', + 'tt', + 'tw', + 'ty', + 'ug', + 'uk', + 'ur', + 'uz', + 've', + 'vi', + 'vo', + 'wa', + 'wo', + 'xh', + 'yi', + 'yo', + 'za', + 'zh', + 'zu', +] as const; + +export type Language = (typeof languages)[number]; diff --git a/backend/src/contents/interceptor/contents.interceptor.ts b/backend/src/contents/interceptor/contents.interceptor.ts new file mode 100644 index 0000000..1e99d20 --- /dev/null +++ b/backend/src/contents/interceptor/contents.interceptor.ts @@ -0,0 +1,46 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, map } from 'rxjs'; +import { UploadService } from 'src/upload/upload.service'; +import { ContentDTO } from '../dto/response/content.dto'; + +@Injectable() +export class ContentsInterceptor implements NestInterceptor { + constructor(private readonly uploadService: UploadService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next + .handle() + .pipe( + map(async (data) => + Array.isArray(data) + ? Promise.all(data.map((project) => this.convertContent(project))) + : this.convertContent(data), + ), + ); + } + + async convertContent(content: ContentDTO) { + const modifiedContent = { ...content }; + if (modifiedContent.file) { + modifiedContent.file = await this.uploadService.createPresignedUrl({ + key: modifiedContent.file, + expires: 60 * 60 * 24 * 7, + }); + } + if ( + !modifiedContent.trailer?.startsWith('http') && + modifiedContent.trailer + ) { + modifiedContent.trailer = await this.uploadService.createPresignedUrl({ + key: modifiedContent.trailer, + expires: 60 * 60 * 24 * 7, + }); + } + return modifiedContent; + } +} diff --git a/backend/src/contents/key.controller.ts b/backend/src/contents/key.controller.ts new file mode 100644 index 0000000..f3ada09 --- /dev/null +++ b/backend/src/contents/key.controller.ts @@ -0,0 +1,67 @@ +import { + Controller, + Get, + Param, + Req, + Res, + UnauthorizedException, + NotFoundException, + UseGuards, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ContentKey } from './entities/content-key.entity'; +import { Response, Request } from 'express'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; + +/** + * AES-128 Key Server for HLS content protection. + * + * HLS players request encryption keys via the #EXT-X-KEY URI in the manifest. + * This endpoint serves those keys, gated behind Nostr JWT authentication. + * + * The key is returned as raw binary (application/octet-stream) -- the standard + * format expected by HLS.js and native HLS players. + */ +@Controller('contents') +export class KeyController { + private readonly logger = new Logger(KeyController.name); + + constructor( + @InjectRepository(ContentKey) + private readonly keyRepository: Repository, + ) {} + + @Get(':id/key') + @UseGuards(HybridAuthGuard) + async getKey( + @Param('id') contentId: string, + @Req() req: Request, + @Res() res: Response, + ) { + // User is authenticated via HybridAuthGuard (Nostr JWT or NIP-98) + // Future: check user's subscription/rental access here + + const key = await this.keyRepository.findOne({ + where: { contentId }, + order: { rotationIndex: 'DESC' }, + }); + + if (!key) { + throw new NotFoundException('Encryption key not found for this content'); + } + + this.logger.log( + `Key served for content ${contentId} (rotation: ${key.rotationIndex})`, + ); + + res.set({ + 'Content-Type': 'application/octet-stream', + 'Content-Length': key.keyData.length.toString(), + 'Cache-Control': 'no-store', + }); + + res.send(key.keyData); + } +} diff --git a/backend/src/contents/types/transcode.ts b/backend/src/contents/types/transcode.ts new file mode 100644 index 0000000..ce9202c --- /dev/null +++ b/backend/src/contents/types/transcode.ts @@ -0,0 +1,10 @@ +export type Transcode = { + inputBucket: string; + outputBucket: string; + inputKey: string; + outputKey: string; + correlationId: string; + callbackUrl: string; + drmContentId?: string; + drmMediaId?: string; +}; diff --git a/backend/src/database/database.module.ts b/backend/src/database/database.module.ts new file mode 100644 index 0000000..95155ee --- /dev/null +++ b/backend/src/database/database.module.ts @@ -0,0 +1,51 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { Unique } from './validators/unique.validator'; + +/** + * Database module. + * Removed the second PostHog database connection. + * Only the main application database is configured. + */ +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + type: 'postgres', + host: configService.get('DATABASE_HOST'), + port: configService.get('DATABASE_PORT'), + username: configService.get('DATABASE_USER'), + password: configService.get('DATABASE_PASSWORD'), + database: configService.get('DATABASE_NAME'), + namingStrategy: new SnakeNamingStrategy(), + entities: ['dist/**/*.entity.{ts,js}'], + migrations: ['dist/database/migrations/*.{ts,js}'], + migrationsTableName: 'typeorm_migrations', + autoLoadEntities: true, + // In production: false. In development: true for auto-schema sync. + synchronize: configService.get('ENVIRONMENT') === 'development', + connectTimeoutMS: 10_000, + maxQueryExecutionTime: 30_000, + poolSize: 50, + extra: { + poolSize: 50, + connectionTimeoutMillis: 5000, + query_timeout: 30_000, + statement_timeout: 30_000, + }, + // No SSL for local/Docker environments + ssl: + configService.get('ENVIRONMENT') === 'local' || + configService.get('ENVIRONMENT') === 'development' + ? false + : { rejectUnauthorized: false }, + }), + }), + ], + providers: [Unique], +}) +export class DatabaseModule {} diff --git a/backend/src/database/migrations/1694527530562-users_table.ts b/backend/src/database/migrations/1694527530562-users_table.ts new file mode 100644 index 0000000..438e702 --- /dev/null +++ b/backend/src/database/migrations/1694527530562-users_table.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UsersTable1694527530562 implements MigrationInterface { + name = 'UsersTable1694527530562'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "users" ("id" character varying NOT NULL, "email" character varying NOT NULL, "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "users"`); + } +} diff --git a/backend/src/database/migrations/1695147061311-films_tables.ts b/backend/src/database/migrations/1695147061311-films_tables.ts new file mode 100644 index 0000000..b6d41f9 --- /dev/null +++ b/backend/src/database/migrations/1695147061311-films_tables.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FilmsTables1695147061311 implements MigrationInterface { + name = 'FilmsTables1695147061311'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "films" ("id" character varying NOT NULL, "title" character varying NOT NULL, "description" character varying, "file" character varying, "trailer" character varying, "poster" character varying, "status" character varying NOT NULL DEFAULT 'draft', "pending_revenue" character varying NOT NULL DEFAULT '0', "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "PK_697487ada088902377482c970d1" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "filmmakers_films" ("filmmaker_id" character varying NOT NULL, "film_id" character varying NOT NULL, "share" integer NOT NULL, "role" character varying NOT NULL, "is_owner" boolean NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_7810850c4a8f129497f8852a483" PRIMARY KEY ("filmmaker_id", "film_id"))`, + ); + await queryRunner.query( + `CREATE TABLE "filmmakers" ("id" character varying NOT NULL, "headshot" character varying, "bio" character varying, "lightning_address" character varying, "user_id" character varying NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_00d2acdbee15ba0c728c7b6218" UNIQUE ("user_id"), CONSTRAINT "PK_f9f33b28ad0402474c529ef6aef" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "FK_c9628a5ebf8dd4f66875c116980" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "FK_b8e108f42fd0d417efa58a4d765" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD CONSTRAINT "FK_00d2acdbee15ba0c728c7b6218a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP CONSTRAINT "FK_00d2acdbee15ba0c728c7b6218a"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "FK_b8e108f42fd0d417efa58a4d765"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "FK_c9628a5ebf8dd4f66875c116980"`, + ); + await queryRunner.query(`DROP TABLE "filmmakers"`); + await queryRunner.query(`DROP TABLE "filmmakers_films"`); + await queryRunner.query(`DROP TABLE "films"`); + } +} diff --git a/backend/src/database/migrations/1695392891033-move_profile_pic.ts b/backend/src/database/migrations/1695392891033-move_profile_pic.ts new file mode 100644 index 0000000..933f379 --- /dev/null +++ b/backend/src/database/migrations/1695392891033-move_profile_pic.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MoveProfilePic1695392891033 implements MigrationInterface { + name = 'MoveProfilePic1695392891033'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "filmmakers" DROP COLUMN "headshot"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "profile_picture_url" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" DROP COLUMN "profile_picture_url"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "headshot" character varying`, + ); + } +} diff --git a/backend/src/database/migrations/1695920604024-fix_timestamp.ts b/backend/src/database/migrations/1695920604024-fix_timestamp.ts new file mode 100644 index 0000000..5cb0a3f --- /dev/null +++ b/backend/src/database/migrations/1695920604024-fix_timestamp.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixTimestamp1695920604024 implements MigrationInterface { + name = 'FixTimestamp1695920604024'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "updated_at"`); + await queryRunner.query( + `ALTER TABLE "films" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "created_at"`); + await queryRunner.query( + `ALTER TABLE "films" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "deleted_at"`); + await queryRunner.query( + `ALTER TABLE "films" ADD "deleted_at" TIMESTAMP WITH TIME ZONE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "deleted_at"`); + await queryRunner.query(`ALTER TABLE "films" ADD "deleted_at" TIMESTAMP`); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "created_at"`); + await queryRunner.query( + `ALTER TABLE "films" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "updated_at"`); + await queryRunner.query( + `ALTER TABLE "films" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + } +} diff --git a/backend/src/database/migrations/1696339984154-add_cast_crew_to_film.ts b/backend/src/database/migrations/1696339984154-add_cast_crew_to_film.ts new file mode 100644 index 0000000..dfb6dc9 --- /dev/null +++ b/backend/src/database/migrations/1696339984154-add_cast_crew_to_film.ts @@ -0,0 +1,79 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCastCrewToFilm1696339984154 implements MigrationInterface { + name = 'AddCastCrewToFilm1696339984154'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" ADD "cast" character varying`); + await queryRunner.query(`ALTER TABLE "films" ADD "crew" character varying`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "updated_at"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "created_at"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "updated_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "updated_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "updated_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "updated_at"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "created_at"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "updated_at"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "crew"`); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "cast"`); + } +} diff --git a/backend/src/database/migrations/1697036073577-removed_role.ts b/backend/src/database/migrations/1697036073577-removed_role.ts new file mode 100644 index 0000000..c4f8e04 --- /dev/null +++ b/backend/src/database/migrations/1697036073577-removed_role.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemovedRole1697036073577 implements MigrationInterface { + name = 'RemovedRole1697036073577'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "role"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "role" character varying NOT NULL`, + ); + } +} diff --git a/backend/src/database/migrations/1697037684328-added_filmmaker_film_id.ts b/backend/src/database/migrations/1697037684328-added_filmmaker_film_id.ts new file mode 100644 index 0000000..2a1f4e8 --- /dev/null +++ b/backend/src/database/migrations/1697037684328-added_filmmaker_film_id.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedFilmmakerFilmId1697037684328 implements MigrationInterface { + name = 'AddedFilmmakerFilmId1697037684328'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "id" SERIAL NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "PK_7810850c4a8f129497f8852a483"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "film_id", "id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "PK_7810850c4a8f129497f8852a483" PRIMARY KEY ("filmmaker_id", "film_id")`, + ); + await queryRunner.query(`ALTER TABLE "filmmakers_films" DROP COLUMN "id"`); + } +} diff --git a/backend/src/database/migrations/1697037772208-added_filmmaker_film_id_primary.ts b/backend/src/database/migrations/1697037772208-added_filmmaker_film_id_primary.ts new file mode 100644 index 0000000..4b4630e --- /dev/null +++ b/backend/src/database/migrations/1697037772208-added_filmmaker_film_id_primary.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedFilmmakerFilmIdPrimary1697037772208 + implements MigrationInterface +{ + name = 'AddedFilmmakerFilmIdPrimary1697037772208'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "PK_7810850c4a8f129497f8852a483" PRIMARY KEY ("filmmaker_id", "film_id")`, + ); + await queryRunner.query(`ALTER TABLE "filmmakers_films" DROP COLUMN "id"`); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "id" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "PK_7810850c4a8f129497f8852a483"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "film_id", "id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "PK_7810850c4a8f129497f8852a483" PRIMARY KEY ("filmmaker_id", "film_id")`, + ); + await queryRunner.query(`ALTER TABLE "filmmakers_films" DROP COLUMN "id"`); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "id" SERIAL NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "PK_7810850c4a8f129497f8852a483"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "film_id", "id")`, + ); + } +} diff --git a/backend/src/database/migrations/1698684215465-add_payments.ts b/backend/src/database/migrations/1698684215465-add_payments.ts new file mode 100644 index 0000000..5157f8a --- /dev/null +++ b/backend/src/database/migrations/1698684215465-add_payments.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPayments1698684215465 implements MigrationInterface { + name = 'AddPayments1698684215465'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "payments" ("id" character varying NOT NULL, "filmmaker_film_id" character varying NOT NULL, "provider_id" character varying NOT NULL, "milisats_amount" integer, "usd_amount" integer, "status" character varying NOT NULL DEFAULT 'pending', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_197ab7af18c93fbb0c9b28b4a59" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "films" DROP COLUMN "pending_revenue"`, + ); + await queryRunner.query( + `ALTER TABLE "films" ADD "pending_revenue" integer NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" DROP COLUMN "pending_revenue"`, + ); + await queryRunner.query( + `ALTER TABLE "films" ADD "pending_revenue" character varying NOT NULL DEFAULT '0'`, + ); + await queryRunner.query(`DROP TABLE "payments"`); + } +} diff --git a/backend/src/database/migrations/1698929920881-change_usd_to_decimal.ts b/backend/src/database/migrations/1698929920881-change_usd_to_decimal.ts new file mode 100644 index 0000000..0a2aeee --- /dev/null +++ b/backend/src/database/migrations/1698929920881-change_usd_to_decimal.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ChangeUsdToDecimal1698929920881 implements MigrationInterface { + name = 'ChangeUsdToDecimal1698929920881'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "usd_amount"`); + await queryRunner.query( + `ALTER TABLE "payments" ADD "usd_amount" numeric(10,4) NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "usd_amount"`); + await queryRunner.query(`ALTER TABLE "payments" ADD "usd_amount" integer`); + } +} diff --git a/backend/src/database/migrations/1699966298556-removed_pending_revenue_film.ts b/backend/src/database/migrations/1699966298556-removed_pending_revenue_film.ts new file mode 100644 index 0000000..7fd3dcd --- /dev/null +++ b/backend/src/database/migrations/1699966298556-removed_pending_revenue_film.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemovedPendingRevenueFilm1699966298556 + implements MigrationInterface +{ + name = 'RemovedPendingRevenueFilm1699966298556'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" DROP COLUMN "pending_revenue"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" ADD "pending_revenue" integer NOT NULL DEFAULT '0'`, + ); + } +} diff --git a/backend/src/database/migrations/1699982200465-created_invitations.ts b/backend/src/database/migrations/1699982200465-created_invitations.ts new file mode 100644 index 0000000..2ab560f --- /dev/null +++ b/backend/src/database/migrations/1699982200465-created_invitations.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreatedInvitations1699982200465 implements MigrationInterface { + name = 'CreatedInvitations1699982200465'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "invites" ("id" character varying NOT NULL, "film_id" character varying NOT NULL, "email" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_79bbf0f768df5e816e421b4c050" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query( + `ALTER TABLE "invites" ADD CONSTRAINT "FK_90b37b2a69026d59f882680da11" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "invites" DROP CONSTRAINT "FK_90b37b2a69026d59f882680da11"`, + ); + await queryRunner.query(`DROP TABLE "invites"`); + } +} diff --git a/backend/src/database/migrations/1700060422508-added_professional_name.ts b/backend/src/database/migrations/1700060422508-added_professional_name.ts new file mode 100644 index 0000000..4da7ac8 --- /dev/null +++ b/backend/src/database/migrations/1700060422508-added_professional_name.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedProfessionalName1700060422508 implements MigrationInterface { + name = 'AddedProfessionalName1700060422508'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "first_name"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "last_name"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "full_name" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "professional_name" character varying NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "professional_name"`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "full_name"`); + await queryRunner.query( + `ALTER TABLE "users" ADD "last_name" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "users" ADD "first_name" character varying NOT NULL`, + ); + } +} diff --git a/backend/src/database/migrations/1700156778683-added_relationship.ts b/backend/src/database/migrations/1700156778683-added_relationship.ts new file mode 100644 index 0000000..da8e49d --- /dev/null +++ b/backend/src/database/migrations/1700156778683-added_relationship.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedRelationship1700156778683 implements MigrationInterface { + name = 'AddedRelationship1700156778683'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD CONSTRAINT "UQ_302549519efea7b1208af57335f" UNIQUE ("id")`, + ); + await queryRunner.query( + `ALTER TABLE "payments" ADD CONSTRAINT "FK_fa6dd27aa07238920f170a7b726" FOREIGN KEY ("filmmaker_film_id") REFERENCES "filmmakers_films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payments" DROP CONSTRAINT "FK_fa6dd27aa07238920f170a7b726"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP CONSTRAINT "UQ_302549519efea7b1208af57335f"`, + ); + } +} diff --git a/backend/src/database/migrations/1700252763989-rename_full_name_to_legal_name.ts b/backend/src/database/migrations/1700252763989-rename_full_name_to_legal_name.ts new file mode 100644 index 0000000..8144335 --- /dev/null +++ b/backend/src/database/migrations/1700252763989-rename_full_name_to_legal_name.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameFullNameToLegalName1700252763989 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" RENAME COLUMN "full_name" TO "legal_name"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" RENAME COLUMN "legal_name" TO "full_name"`, + ); + } +} diff --git a/backend/src/database/migrations/1700593416123-rename_description_to_synopsis.ts b/backend/src/database/migrations/1700593416123-rename_description_to_synopsis.ts new file mode 100644 index 0000000..294377f --- /dev/null +++ b/backend/src/database/migrations/1700593416123-rename_description_to_synopsis.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameDescriptionToSynopsis1700593416123 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" RENAME COLUMN "description" TO "synopsis"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" RENAME COLUMN "synopsis" TO "description"`, + ); + } +} diff --git a/backend/src/database/migrations/1701199616013-add_pending_revenue.ts b/backend/src/database/migrations/1701199616013-add_pending_revenue.ts new file mode 100644 index 0000000..efaf805 --- /dev/null +++ b/backend/src/database/migrations/1701199616013-add_pending_revenue.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPendingRevenue1701199616013 implements MigrationInterface { + name = 'AddPendingRevenue1701199616013'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "pending_revenue" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "pending_revenue"`, + ); + } +} diff --git a/backend/src/database/migrations/1701454162558-fix-delete-filmmaker.ts b/backend/src/database/migrations/1701454162558-fix-delete-filmmaker.ts new file mode 100644 index 0000000..077247d --- /dev/null +++ b/backend/src/database/migrations/1701454162558-fix-delete-filmmaker.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixDeleteFilmmaker1701454162558 implements MigrationInterface { + name = 'FixDeleteFilmmaker1701454162558'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" ADD "deleted_at" TIMESTAMP WITH TIME ZONE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" DROP COLUMN "deleted_at"`, + ); + } +} diff --git a/backend/src/database/migrations/1702566370518-rename-filmmaker-film.ts b/backend/src/database/migrations/1702566370518-rename-filmmaker-film.ts new file mode 100644 index 0000000..c380d58 --- /dev/null +++ b/backend/src/database/migrations/1702566370518-rename-filmmaker-film.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameFilmmakerFilm1702566370518 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers_films" RENAME TO "shareholders"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" RENAME COLUMN "filmmaker_film_id" TO "shareholder_id"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" RENAME TO "filmmakers_films"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" RENAME COLUMN "shareholder_id" TO "filmmaker_film_id"`, + ); + } +} diff --git a/backend/src/database/migrations/1702915082800-added-cast-crew.ts b/backend/src/database/migrations/1702915082800-added-cast-crew.ts new file mode 100644 index 0000000..90c94c5 --- /dev/null +++ b/backend/src/database/migrations/1702915082800-added-cast-crew.ts @@ -0,0 +1,77 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedCastCrew1702915082800 implements MigrationInterface { + name = 'AddedCastCrew1702915082800'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_c9628a5ebf8dd4f66875c116980"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_b8e108f42fd0d417efa58a4d765"`, + ); + await queryRunner.query( + `CREATE TABLE "crews" ("id" character varying NOT NULL, "filmmaker_id" character varying NOT NULL, "film_id" character varying NOT NULL, "role" integer NOT NULL, "occupation" integer NOT NULL, "order" integer NOT NULL, "email" character varying NOT NULL, "pending_revenue" numeric(10,2) NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_82339ba2ac2655387277341883f" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query( + `CREATE TABLE "casts" ("id" character varying NOT NULL, "filmmaker_id" character varying NOT NULL, "film_id" character varying NOT NULL, "character" character varying NOT NULL, "order" integer NOT NULL, "email" character varying NOT NULL, "pending_revenue" numeric(10,2) NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_481951f875efb1e4e07812e25c5" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "cast"`); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "crew"`); + await queryRunner.query( + `ALTER TABLE "payments" ADD "filmmaker_film_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_a731a85e00d9820abe2332924e9" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_8f8a4ee33f8afe7b90bc6b4c0a6" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD CONSTRAINT "FK_05c86d3dc89a4e0ca340336ad06" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD CONSTRAINT "FK_fcd6379cdf6bad96ad5bed0e8d2" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD CONSTRAINT "FK_6c75f4d666f344dc2bdeda6b3c8" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD CONSTRAINT "FK_0c808b1f5954f34ba80e18e934c" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "casts" DROP CONSTRAINT "FK_0c808b1f5954f34ba80e18e934c"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" DROP CONSTRAINT "FK_6c75f4d666f344dc2bdeda6b3c8"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" DROP CONSTRAINT "FK_fcd6379cdf6bad96ad5bed0e8d2"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" DROP CONSTRAINT "FK_05c86d3dc89a4e0ca340336ad06"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_8f8a4ee33f8afe7b90bc6b4c0a6"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_a731a85e00d9820abe2332924e9"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" DROP COLUMN "filmmaker_film_id"`, + ); + await queryRunner.query(`ALTER TABLE "films" ADD "crew" character varying`); + await queryRunner.query(`ALTER TABLE "films" ADD "cast" character varying`); + await queryRunner.query(`DROP TABLE "casts"`); + await queryRunner.query(`DROP TABLE "crews"`); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_b8e108f42fd0d417efa58a4d765" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_c9628a5ebf8dd4f66875c116980" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1702916300496-make-nullable-filmmaker-id.ts b/backend/src/database/migrations/1702916300496-make-nullable-filmmaker-id.ts new file mode 100644 index 0000000..674d3dc --- /dev/null +++ b/backend/src/database/migrations/1702916300496-make-nullable-filmmaker-id.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MakeNullableFilmmakerId1702916300496 + implements MigrationInterface +{ + name = 'MakeNullableFilmmakerId1702916300496'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "crews" DROP CONSTRAINT "FK_05c86d3dc89a4e0ca340336ad06"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ALTER COLUMN "filmmaker_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "casts" DROP CONSTRAINT "FK_6c75f4d666f344dc2bdeda6b3c8"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ALTER COLUMN "filmmaker_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD CONSTRAINT "FK_05c86d3dc89a4e0ca340336ad06" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD CONSTRAINT "FK_6c75f4d666f344dc2bdeda6b3c8" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "casts" DROP CONSTRAINT "FK_6c75f4d666f344dc2bdeda6b3c8"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" DROP CONSTRAINT "FK_05c86d3dc89a4e0ca340336ad06"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ALTER COLUMN "filmmaker_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD CONSTRAINT "FK_6c75f4d666f344dc2bdeda6b3c8" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ALTER COLUMN "filmmaker_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD CONSTRAINT "FK_05c86d3dc89a4e0ca340336ad06" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1702917298362-make-nullable-email.ts b/backend/src/database/migrations/1702917298362-make-nullable-email.ts new file mode 100644 index 0000000..65d4af0 --- /dev/null +++ b/backend/src/database/migrations/1702917298362-make-nullable-email.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MakeNullableEmail1702917298362 implements MigrationInterface { + name = 'MakeNullableEmail1702917298362'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "casts" DROP COLUMN "pending_revenue"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ALTER COLUMN "email" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "casts" ALTER COLUMN "email" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD "pending_revenue" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } +} diff --git a/backend/src/database/migrations/1703106184441-add-crew.ts b/backend/src/database/migrations/1703106184441-add-crew.ts new file mode 100644 index 0000000..6c86895 --- /dev/null +++ b/backend/src/database/migrations/1703106184441-add-crew.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCrew1703106184441 implements MigrationInterface { + name = 'AddCrew1703106184441'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "crews" DROP COLUMN "role"`); + await queryRunner.query( + `ALTER TABLE "crews" DROP COLUMN "pending_revenue"`, + ); + await queryRunner.query(`ALTER TABLE "crews" DROP COLUMN "occupation"`); + await queryRunner.query( + `ALTER TABLE "crews" ADD "occupation" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ALTER COLUMN "email" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "crews" ALTER COLUMN "email" SET NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "crews" DROP COLUMN "occupation"`); + await queryRunner.query( + `ALTER TABLE "crews" ADD "occupation" integer NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD "pending_revenue" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + await queryRunner.query(`ALTER TABLE "crews" ADD "role" integer NOT NULL`); + } +} diff --git a/backend/src/database/migrations/1705345231915-added-subscriptions.ts b/backend/src/database/migrations/1705345231915-added-subscriptions.ts new file mode 100644 index 0000000..21590b2 --- /dev/null +++ b/backend/src/database/migrations/1705345231915-added-subscriptions.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedSubscriptions1705345231915 implements MigrationInterface { + name = 'AddedSubscriptions1705345231915'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "subscriptions" ("id" character varying NOT NULL, "stripe_id" character varying, "status" character varying NOT NULL, "user_id" character varying NOT NULL, "type" character varying NOT NULL, "period" character varying NOT NULL, "period_end" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_a87248d73155605cf782be9ee5e" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "subscriptions" ADD CONSTRAINT "FK_d0a95ef8a28188364c546eb65c1" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" DROP CONSTRAINT "FK_d0a95ef8a28188364c546eb65c1"`, + ); + await queryRunner.query(`DROP TABLE "subscriptions"`); + } +} diff --git a/backend/src/database/migrations/1706558880071-added-project-tables.ts b/backend/src/database/migrations/1706558880071-added-project-tables.ts new file mode 100644 index 0000000..229ad7a --- /dev/null +++ b/backend/src/database/migrations/1706558880071-added-project-tables.ts @@ -0,0 +1,79 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedProjectTables1706558880071 implements MigrationInterface { + name = 'AddedProjectTables1706558880071'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payments" DROP CONSTRAINT "FK_fa6dd27aa07238920f170a7b726"`, + ); + await queryRunner.query( + `CREATE TABLE "festivals" ("id" character varying NOT NULL, "name" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_6d4d298db683d281bcaed953a46" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "festival_screenings" ("id" character varying NOT NULL, "film_id" character varying NOT NULL, "festival_id" character varying NOT NULL, "year" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_cfec9a37e1774cd1c77c0e78d93" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query( + `CREATE TABLE "documents" ("id" character varying NOT NULL, "film_id" character varying NOT NULL, "name" character varying NOT NULL, "url" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_c6952b0a27da64ed67e193ca1f3" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query( + `CREATE TABLE "awards" ("id" character varying NOT NULL, "film_id" character varying NOT NULL, "award_issuer_id" character varying NOT NULL, "name" character varying NOT NULL, "year" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_2517c15a5bc76c5055f9b909261" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query( + `CREATE TABLE "award_issuers" ("id" character varying NOT NULL, "name" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_4435fba8ba1e78161aa900e313c" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "payments" DROP COLUMN "filmmaker_film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" ADD CONSTRAINT "FK_733595cfde6a11d4f4f2b6c7b4b" FOREIGN KEY ("shareholder_id") REFERENCES "shareholders"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" ADD CONSTRAINT "FK_b72da3cfc0781b8e8bf0ec07b55" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" ADD CONSTRAINT "FK_64ed0c2ab4ba23d5ad977eba0bb" FOREIGN KEY ("festival_id") REFERENCES "festivals"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "documents" ADD CONSTRAINT "FK_97a23ecdb3849289ba88204317f" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "awards" ADD CONSTRAINT "FK_f9bd206872ffb6401f9992eef3b" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "awards" ADD CONSTRAINT "FK_8fdac39f81cd4cca0557a485c2d" FOREIGN KEY ("award_issuer_id") REFERENCES "award_issuers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "awards" DROP CONSTRAINT "FK_8fdac39f81cd4cca0557a485c2d"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" DROP CONSTRAINT "FK_f9bd206872ffb6401f9992eef3b"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" DROP CONSTRAINT "FK_97a23ecdb3849289ba88204317f"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" DROP CONSTRAINT "FK_64ed0c2ab4ba23d5ad977eba0bb"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" DROP CONSTRAINT "FK_b72da3cfc0781b8e8bf0ec07b55"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" DROP CONSTRAINT "FK_733595cfde6a11d4f4f2b6c7b4b"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" ADD "filmmaker_film_id" character varying`, + ); + await queryRunner.query(`DROP TABLE "award_issuers"`); + await queryRunner.query(`DROP TABLE "awards"`); + await queryRunner.query(`DROP TABLE "documents"`); + await queryRunner.query(`DROP TABLE "festival_screenings"`); + await queryRunner.query(`DROP TABLE "festivals"`); + await queryRunner.query( + `ALTER TABLE "payments" ADD CONSTRAINT "FK_fa6dd27aa07238920f170a7b726" FOREIGN KEY ("shareholder_id") REFERENCES "shareholders"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1706713872230-added-new-film-details.ts b/backend/src/database/migrations/1706713872230-added-new-film-details.ts new file mode 100644 index 0000000..40f25f2 --- /dev/null +++ b/backend/src/database/migrations/1706713872230-added-new-film-details.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedNewFilmDetails1706713872230 implements MigrationInterface { + name = 'AddedNewFilmDetails1706713872230'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" ADD "project_name" character varying NOT NULL DEFAULT ''`, + ); + await queryRunner.query(`ALTER TABLE "films" ADD "type" character varying`); + await queryRunner.query( + `ALTER TABLE "films" ADD "format" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "format"`); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "type"`); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "project_name"`); + } +} diff --git a/backend/src/database/migrations/1706888680949-added-genres.ts b/backend/src/database/migrations/1706888680949-added-genres.ts new file mode 100644 index 0000000..1d49004 --- /dev/null +++ b/backend/src/database/migrations/1706888680949-added-genres.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedGenres1706888680949 implements MigrationInterface { + name = 'AddedGenres1706888680949'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "genres" ("id" character varying NOT NULL, "name" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_80ecd718f0f00dde5d77a9be842" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "film_genres" ("id" character varying NOT NULL, "film_id" character varying NOT NULL, "genre_id" character varying NOT NULL, "year" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_aa48135bfcc893e3ebfdc416a60" PRIMARY KEY ("id", "film_id"))`, + ); + await queryRunner.query( + `ALTER TABLE "film_genres" ADD CONSTRAINT "FK_9b6ca3be1e09e7537b24144d21d" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "film_genres" ADD CONSTRAINT "FK_d273e8fd854a03f655a751f393e" FOREIGN KEY ("genre_id") REFERENCES "genres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "film_genres" DROP CONSTRAINT "FK_d273e8fd854a03f655a751f393e"`, + ); + await queryRunner.query( + `ALTER TABLE "film_genres" DROP CONSTRAINT "FK_9b6ca3be1e09e7537b24144d21d"`, + ); + await queryRunner.query(`DROP TABLE "film_genres"`); + await queryRunner.query(`DROP TABLE "genres"`); + } +} diff --git a/backend/src/database/migrations/1706907277985-fix-genre-table.ts b/backend/src/database/migrations/1706907277985-fix-genre-table.ts new file mode 100644 index 0000000..e5d7ab9 --- /dev/null +++ b/backend/src/database/migrations/1706907277985-fix-genre-table.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixGenreTable1706907277985 implements MigrationInterface { + name = 'FixGenreTable1706907277985'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "film_genres" DROP COLUMN "year"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "film_genres" ADD "year" integer NOT NULL`, + ); + } +} diff --git a/backend/src/database/migrations/1707144468805-added-placeholder_name-cast-crew.ts b/backend/src/database/migrations/1707144468805-added-placeholder_name-cast-crew.ts new file mode 100644 index 0000000..294a92f --- /dev/null +++ b/backend/src/database/migrations/1707144468805-added-placeholder_name-cast-crew.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedPlaceholderNameCastCrew1707144468805 + implements MigrationInterface +{ + name = 'AddedPlaceholderNameCastCrew1707144468805'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "casts" ADD "placeholder_name" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD "placeholder_name" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "crews" DROP COLUMN "placeholder_name"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" DROP COLUMN "placeholder_name"`, + ); + } +} diff --git a/backend/src/database/migrations/1707234606168-added-shares-to-invites.ts b/backend/src/database/migrations/1707234606168-added-shares-to-invites.ts new file mode 100644 index 0000000..b0cf166 --- /dev/null +++ b/backend/src/database/migrations/1707234606168-added-shares-to-invites.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedSharesToInvites1707234606168 implements MigrationInterface { + name = 'AddedSharesToInvites1707234606168'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "invites" ADD "share" integer NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "invites" DROP COLUMN "share"`); + } +} diff --git a/backend/src/database/migrations/1707246772876-added-slug-film.ts b/backend/src/database/migrations/1707246772876-added-slug-film.ts new file mode 100644 index 0000000..fa5b474 --- /dev/null +++ b/backend/src/database/migrations/1707246772876-added-slug-film.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedSlugFilm1707246772876 implements MigrationInterface { + name = 'AddedSlugFilm1707246772876'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" ADD "slug" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "slug"`); + } +} diff --git a/backend/src/database/migrations/1707314864786-added-permissions.ts b/backend/src/database/migrations/1707314864786-added-permissions.ts new file mode 100644 index 0000000..43900a3 --- /dev/null +++ b/backend/src/database/migrations/1707314864786-added-permissions.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedPermissions1707314864786 implements MigrationInterface { + name = 'AddedPermissions1707314864786'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "permissions" ("id" character varying NOT NULL, "filmmaker_id" character varying NOT NULL, "film_id" character varying NOT NULL, "role" character varying NOT NULL, CONSTRAINT "id" UNIQUE ("id"), CONSTRAINT "PK_568e9d106e96fbcb887c10b334c" PRIMARY KEY ("id", "filmmaker_id", "film_id"))`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_287711659c1379214f4e851d8be" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_29e02d061b7c91002165230d636" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_29e02d061b7c91002165230d636"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_287711659c1379214f4e851d8be"`, + ); + await queryRunner.query(`DROP TABLE "permissions"`); + } +} diff --git a/backend/src/database/migrations/1707408634005-seed-festivals.ts b/backend/src/database/migrations/1707408634005-seed-festivals.ts new file mode 100644 index 0000000..d5b291b --- /dev/null +++ b/backend/src/database/migrations/1707408634005-seed-festivals.ts @@ -0,0 +1,356 @@ +import { Festival } from 'src/festivals/entities/festival.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeedFestivals1707408634005 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const festivals = [ + { + id: '3e8b0ff3-94e1-437b-a6f7-1800f5e6638d', + name: 'Abu Dhabi Film Festival', + }, + { + id: 'f4f15a9b-6ad9-441a-a4d8-2ed043a9ebdb', + name: 'AFI Docs Film Festival', + }, + { + id: '7369f3f0-1d8d-4a85-96d4-4fe1c545a6f3', + name: 'AFI Fest', + }, + { + id: 'b364c891-b70d-43a5-8db4-c1de73a86171', + name: 'Ann Arbor Film Fest', + }, + { + id: 'ff182d42-dad4-4093-8d97-616ee0bb7b09', + name: 'Ashland Independent Film Festival', + }, + { + id: '1e22923a-d2ac-4ad9-8331-1629f2e924f3', + name: 'BAMCinemafest', + }, + { + id: 'e2f708ae-5d78-4f3f-b3b4-8372426c8ed1', + name: 'Berlin International Film Festival (Berlinale)', + }, + { + id: 'c9478678-61a0-4566-aa10-1182cb4a5b3a', + name: 'BFI London Film Festival', + }, + { + id: 'e4c7c28f-c714-4424-850f-320fc0e8e928', + name: 'Big Sky Documentary Film Festival', + }, + { + id: '89a7a11d-0f7d-42b4-8376-25c74a4d17d0', + name: 'Buenos Aires International Festival of Independent Film', + }, + { + id: '69b19535-d7db-4e91-9070-b5173cda0b61', + name: 'Busan International Film Festival', + }, + { + id: 'db0b4308-c797-4c2f-bf24-826b18d8e9d5', + name: 'Cairo International Film Festival', + }, + { + id: '3b324c4c-3e42-46ac-b0e6-5e85d8d50152', + name: 'Camden International Film Festival', + }, + { + id: 'b40f8a27-7f45-44a7-a452-b2959f7d7c88', + name: 'Cannes Film Festival', + }, + { + id: '09c5f0c2-d07c-4847-9109-8f3a5b1d69b6', + name: 'Cannes Film Festival ACID', + }, + { + id: 'c298ae2d-51aa-4b02-80f7-754fcd43a548', + name: 'Cannes Film Festival Cinéfondation', + }, + { + id: 'ce0fc6d0-fbf8-49b1-bf34-b80d0ae8e0de', + name: 'Cannes Film Festival Official Selection', + }, + { + id: 'eddb0d7b-b3d8-4044-9a6f-352d1593e9df', + name: 'Cannes Film Festival Out of Competition', + }, + { + id: '5b78961f-26bc-4526-97a2-b7050a9f26a3', + name: "Cannes Film Festival Quinzaine des Réalisateurs (Director's Fortnight)", + }, + { + id: 'b60ff4f3-2b25-47f5-bcfd-e0e2c5d5e5b6', + name: "Cannes Film Festival Semaine de la Critique (Critic's Week)", + }, + { + id: 'b3b4b742-af20-4c0f-8df9-2d87f7cf5ec7', + name: 'Cannes Film Festival Un Certain Regard', + }, + { + id: '537d6e2a-1ff1-4e97-91d0-83dbb71ab18b', + name: 'Chicago International Film Festival', + }, + { + id: '66e389d7-8e9a-44f0-aa5d-eeb8a56c37e1', + name: 'Chicago Underground Film Festival', + }, + { + id: '5af4e4a6-24a9-46af-87b1-770c9a3a8cc1', + name: 'Cinequest Film Festival', + }, + { + id: 'b04e7c7c-5c31-4b52-8e3c-7c0dc0a9a2d4', + name: 'Clermont-Ferrand International Short Film Festival', + }, + { + id: '2a057956-2ba1-4902-b6a2-d4391bc6a4c1', + name: 'CPH:Dox Documentary Film Festival', + }, + { + id: 'c8759f0a-d4e2-4d76-b1a9-dcfb9c2c92c3', + name: 'Cucalorus International Film Festival', + }, + { + id: 'd6ac5b11-05f3-4a70-92ac-5f81bb4a0cf9', + name: 'Dances with Films', + }, + { + id: 'e586cd4c-2032-4f1b-ae5a-5ec1331bde56', + name: 'DocNYC', + }, + { + id: '00a95394-78b0-403b-b39f-b89352f2f8b8', + name: 'Docpoint', + }, + { + id: 'ab3f421f-1232-4693-830e-97a7bfe9c37f', + name: 'DokLeipzig', + }, + { + id: 'a700cb99-c8ec-4464-8365-3dd4a6e9829a', + name: 'Dubai International Film Festival', + }, + { + id: 'f6df0ee5-df01-41e8-9cfb-0c648a7f8400', + name: 'Edinburgh International Film Festival', + }, + { + id: '44e68d8a-5f14-4f65-b69e-c4a6658da115', + name: 'Fantasia International Film Festival', + }, + { + id: '4b36e429-199a-4817-83f6-7412363b711f', + name: 'Fantastic Fest', + }, + { + id: '8a7b4564-7680-41a9-96b2-6e8daacae755', + name: 'Festival del film Locarno', + }, + { + id: 'a287c57c-64b5-4c02-8e51-9e1814448c2f', + name: 'Festival Internacional de Cine de Morelia (Morelia Film Fest)', + }, + { + id: '6e417254-342a-4935-8e0f-5a6f0771a6a4', + name: 'Frameline', + }, + { + id: '84be9b26-b4f7-4ab1-86d7-0f874f04c1ae', + name: 'Full Frame Documentary Film Festival', + }, + { + id: '68be5fc3-b7f1-429a-ae68-5a24030cfb98', + name: 'Galway Film Fleadh', + }, + { + id: '96ebef61-788a-45ac-8d50-3545a4be5d9c', + name: 'Glasgow Film Festival', + }, + { + id: '55f9c41a-e8fb-47c2-a4a1-3c5d3f0be368', + name: 'Gothenburg Film Festival', + }, + { + id: '32501e43-c4b8-4859-91bc-bd865e5e22c1', + name: 'Guadalajara International Film Festival', + }, + { + id: '3a320d77-dfe7-4cb3-b62b-c8b35a2b1f2f', + name: 'Hamptons International Film Festival', + }, + { + id: '7b01fabe-10b4-47f3-844a-7366dabfe6c8', + name: 'Hot Docs Canadian International Documentary Festival', + }, + { + id: 'ff38d41b-58d4-450b-8e36-086b810cd9e5', + name: 'IFP Week', + }, + { + id: 'b4e072e6-38f8-4f47-bc91-f3e7ef55b3a0', + name: 'International Documentary Film Festival Amsterdam', + }, + { + id: '56c02ef9-8f59-4aee-aa36-70a69482807c', + name: 'International Film Festival Rotterdam', + }, + { + id: 'e69be77e-8128-4e29-a1aa-215b14e55633', + name: 'Karlovy Vary International Film Festival', + }, + { + id: '9719b9a4-6374-4a0d-8129-61c50951a271', + name: 'Los Angeles Film Festival', + }, + { + id: '9c550c61-46a7-4416-81b0-190f0e8e4126', + name: 'Montclair Film Festival', + }, + { + id: 'cefe282e-d860-4f63-83c2-276ee3cd3e51', + name: 'New Directors/New Films | MoMA', + }, + { + id: '8890449d-dae1-41f8-b163-5f228f03a8b1', + name: 'New York Film Festival', + }, + { + id: 'c9b59f0a-d4e2-4d76-b1a9-dcfb9c2c92c3', + name: 'Outfest', + }, + { + id: '2f8b79cc-11b0-4688-89b0-34d6fd5dfe4f', + name: 'Palm Springs International Film Festival', + }, + { + id: '9c5d9a17-c68a-4688-99b5-724a906b13e8', + name: 'Panafrican Film and Television Festival of Ouagadougou', + }, + { + id: '7864ff63-dc49-48d8-bf8b-72a82c4bc5e7', + name: 'Raindance Film Festival', + }, + { + id: '7f4fcd1e-79bb-4e02-9070-4c5b47ff360a', + name: 'Rooftop Films Summer Series', + }, + { + id: '5e3ae402-58a2-48b0-b505-63c32b3f7bb1', + name: 'San Francisco International Film Festival', + }, + { + id: 'f1c883a2-ec60-49df-96cb-ea77658de16c', + name: 'San Francisco Jewish Film Festival', + }, + { + id: '831f6f9e-c8d3-477d-9fd9-240f7b5b4641', + name: 'San Sebastian International Film Festival', + }, + { + id: 'f796b25e-07f3-4695-b3bb-5ee49764c917', + name: 'Sarajevo Film Festival', + }, + { + id: '932f8775-23b0-4f97-a1e3-545d7fca4ad8', + name: 'Sarasota Film Festival', + }, + { + id: '44f8c82a-3018-4f25-a5ab-d3106b5c4857', + name: 'Seattle International Film Festival', + }, + { + id: 'd28809c1-486a-4e89-9323-0ac7a73b1d0d', + name: 'Sedona Film Festival', + }, + { + id: '214fa7a5-2e8f-4c36-96a1-6e212e18a649', + name: 'Sheffield Doc/Fest', + }, + { + id: 'd0a4389e-e180-43eb-bf68-25d3f943b7f5', + name: 'Sitges Film Festival', + }, + { + id: 'd83d706d-34cb-4661-8850-e4e8c324f1f4', + name: 'Slamdance Film Festival', + }, + { + id: 'f3e6ad6a-4b78-441b-973d-46296d7a6c98', + name: 'Stanley Film Festival', + }, + { + id: '69782df3-5d12-4e6c-9963-c2f4e1342899', + name: 'Sundance Film Festival', + }, + { + id: 'c2c27cd3-8c43-4d51-9a22-05b0639448e7', + name: 'Sundance Film Festival New Frontiers', + }, + { + id: 'b925675b-3f4d-4d89-83f5-0e8f261f0d51', + name: 'Sundance Film Festival NEXT', + }, + { + id: '6d53d0eb-22fc-44e6-8ac2-4e66f9208f56', + name: 'Sundance Film Festival World Cinema', + }, + { + id: 'c83d04b7-296e-4d9b-8791-1093c41a0f92', + name: 'Sundance NEXT', + }, + { + id: 'caecb65a-38b7-4929-b0f1-f9b58e4239d8', + name: 'SXSW Film Festival', + }, + { + id: '61e86416-93af-4647-ba68-cb1d97d32e4c', + name: 'Telluride Film Festival', + }, + { + id: '6b9e1468-4e77-4f2e-b742-18c62a27e8c6', + name: 'Thessaloniki International Film Festival', + }, + { + id: 'b0a2c456-95de-449f-81df-e10e0262e250', + name: 'Toronto International Film Festival', + }, + { + id: 'b284e8d9-b186-4c54-9972-fa0f0ffae94d', + name: 'Tribeca Film Festival', + }, + { + id: 'b046b244-0b58-4a85-855f-2f7785ec4017', + name: 'True/False Documentary Film Festival', + }, + { + id: 'a6a4828a-10ae-44c4-b6c8-8a1c6b42a182', + name: 'Vancouver International Film Festival', + }, + { + id: '6e2be543-f055-4a28-b21e-7a2b1b3c0422', + name: 'Venice International Film Festival', + }, + { + id: '5c9d0ac7-61fc-4b7c-b9b3-ef1f95c04b4b', + name: 'Vienna International Film Festival', + }, + { + id: 'e6316d25-0888-4923-a7ac-06eb90083bb2', + name: 'Bitcoin Film Fest', + }, + ]; + + await queryRunner.manager.save( + queryRunner.manager.create>( + Festival, + festivals, + ), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.delete(Festival, {}); + } +} diff --git a/backend/src/database/migrations/1707420455840-seed-award-issuers.ts b/backend/src/database/migrations/1707420455840-seed-award-issuers.ts new file mode 100644 index 0000000..7b47e34 --- /dev/null +++ b/backend/src/database/migrations/1707420455840-seed-award-issuers.ts @@ -0,0 +1,384 @@ +import { AwardIssuer } from 'src/award-issuers/entities/award-issuer.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeedAwardIssuers1707420455840 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const issuers = [ + { + id: 'd67e47ae-84d0-4b85-9323-3c7745a50c44', + name: 'Academy Awards', + }, + { + id: 'ebd29c58-9e0f-439a-9b15-1be7a0f7336d', + name: 'European Film Awards', + }, + { + id: '8f46b0ad-32b6-4f10-8b5a-268049a29ff0', + name: 'Cinema Eye Awards', + }, + { + id: '28f6fc35-b28d-4261-8d9e-4d3022a44505', + name: 'Golden Globes', + }, + { + id: '02792df0-36d4-46e8-a38a-4540e50fc492', + name: 'Emmys', + }, + { + id: '0b4a8c87-bca7-4a8a-9b80-23d7509a6c30', + name: 'Gotham Awards', + }, + { + id: 'c007e438-bb6c-4ed3-8397-2c3e678f5d32', + name: 'IDA Awards', + }, + { + id: 'a0f39639-45d1-4eac-a12c-141a087f3b07', + name: 'Independent Spirit Awards', + }, + { + id: '3e8b0ff3-94e1-437b-a6f7-1800f5e6638d', + name: 'Abu Dhabi Film Festival', + }, + { + id: 'f4f15a9b-6ad9-441a-a4d8-2ed043a9ebdb', + name: 'AFI Docs Film Festival', + }, + { + id: '7369f3f0-1d8d-4a85-96d4-4fe1c545a6f3', + name: 'AFI Fest', + }, + { + id: 'b364c891-b70d-43a5-8db4-c1de73a86171', + name: 'Ann Arbor Film Fest', + }, + { + id: 'ff182d42-dad4-4093-8d97-616ee0bb7b09', + name: 'Ashland Independent Film Festival', + }, + { + id: '1e22923a-d2ac-4ad9-8331-1629f2e924f3', + name: 'BAMCinemafest', + }, + { + id: 'e2f708ae-5d78-4f3f-b3b4-8372426c8ed1', + name: 'Berlin International Film Festival (Berlinale)', + }, + { + id: 'c9478678-61a0-4566-aa10-1182cb4a5b3a', + name: 'BFI London Film Festival', + }, + { + id: 'e4c7c28f-c714-4424-850f-320fc0e8e928', + name: 'Big Sky Documentary Film Festival', + }, + { + id: '89a7a11d-0f7d-42b4-8376-25c74a4d17d0', + name: 'Buenos Aires International Festival of Independent Film', + }, + { + id: '69b19535-d7db-4e91-9070-b5173cda0b61', + name: 'Busan International Film Festival', + }, + { + id: 'db0b4308-c797-4c2f-bf24-826b18d8e9d5', + name: 'Cairo International Film Festival', + }, + { + id: '3b324c4c-3e42-46ac-b0e6-5e85d8d50152', + name: 'Camden International Film Festival', + }, + { + id: 'b40f8a27-7f45-44a7-a452-b2959f7d7c88', + name: 'Cannes Film Festival', + }, + { + id: '09c5f0c2-d07c-4847-9109-8f3a5b1d69b6', + name: 'Cannes Film Festival ACID', + }, + { + id: 'c298ae2d-51aa-4b02-80f7-754fcd43a548', + name: 'Cannes Film Festival Cinéfondation', + }, + { + id: 'ce0fc6d0-fbf8-49b1-bf34-b80d0ae8e0de', + name: 'Cannes Film Festival Official Selection', + }, + { + id: 'eddb0d7b-b3d8-4044-9a6f-352d1593e9df', + name: 'Cannes Film Festival Out of Competition', + }, + { + id: '5b78961f-26bc-4526-97a2-b7050a9f26a3', + name: "Cannes Film Festival Quinzaine des Réalisateurs (Director's Fortnight)", + }, + { + id: 'b60ff4f3-2b25-47f5-bcfd-e0e2c5d5e5b6', + name: "Cannes Film Festival Semaine de la Critique (Critic's Week)", + }, + { + id: 'b3b4b742-af20-4c0f-8df9-2d87f7cf5ec7', + name: 'Cannes Film Festival Un Certain Regard', + }, + { + id: '537d6e2a-1ff1-4e97-91d0-83dbb71ab18b', + name: 'Chicago International Film Festival', + }, + { + id: '66e389d7-8e9a-44f0-aa5d-eeb8a56c37e1', + name: 'Chicago Underground Film Festival', + }, + { + id: '5af4e4a6-24a9-46af-87b1-770c9a3a8cc1', + name: 'Cinequest Film Festival', + }, + { + id: 'b04e7c7c-5c31-4b52-8e3c-7c0dc0a9a2d4', + name: 'Clermont-Ferrand International Short Film Festival', + }, + { + id: '2a057956-2ba1-4902-b6a2-d4391bc6a4c1', + name: 'CPH:Dox Documentary Film Festival', + }, + { + id: 'c8759f0a-d4e2-4d76-b1a9-dcfb9c2c92c3', + name: 'Cucalorus International Film Festival', + }, + { + id: 'd6ac5b11-05f3-4a70-92ac-5f81bb4a0cf9', + name: 'Dances with Films', + }, + { + id: 'e586cd4c-2032-4f1b-ae5a-5ec1331bde56', + name: 'DocNYC', + }, + { + id: '00a95394-78b0-403b-b39f-b89352f2f8b8', + name: 'Docpoint', + }, + { + id: 'ab3f421f-1232-4693-830e-97a7bfe9c37f', + name: 'DokLeipzig', + }, + { + id: 'a700cb99-c8ec-4464-8365-3dd4a6e9829a', + name: 'Dubai International Film Festival', + }, + { + id: 'f6df0ee5-df01-41e8-9cfb-0c648a7f8400', + name: 'Edinburgh International Film Festival', + }, + { + id: '44e68d8a-5f14-4f65-b69e-c4a6658da115', + name: 'Fantasia International Film Festival', + }, + { + id: '4b36e429-199a-4817-83f6-7412363b711f', + name: 'Fantastic Fest', + }, + { + id: '8a7b4564-7680-41a9-96b2-6e8daacae755', + name: 'Festival del film Locarno', + }, + { + id: 'a287c57c-64b5-4c02-8e51-9e1814448c2f', + name: 'Festival Internacional de Cine de Morelia (Morelia Film Fest)', + }, + { + id: '6e417254-342a-4935-8e0f-5a6f0771a6a4', + name: 'Frameline', + }, + { + id: '84be9b26-b4f7-4ab1-86d7-0f874f04c1ae', + name: 'Full Frame Documentary Film Festival', + }, + { + id: '68be5fc3-b7f1-429a-ae68-5a24030cfb98', + name: 'Galway Film Fleadh', + }, + { + id: '96ebef61-788a-45ac-8d50-3545a4be5d9c', + name: 'Glasgow Film Festival', + }, + { + id: '55f9c41a-e8fb-47c2-a4a1-3c5d3f0be368', + name: 'Gothenburg Film Festival', + }, + { + id: '32501e43-c4b8-4859-91bc-bd865e5e22c1', + name: 'Guadalajara International Film Festival', + }, + { + id: '3a320d77-dfe7-4cb3-b62b-c8b35a2b1f2f', + name: 'Hamptons International Film Festival', + }, + { + id: '7b01fabe-10b4-47f3-844a-7366dabfe6c8', + name: 'Hot Docs Canadian International Documentary Festival', + }, + { + id: 'ff38d41b-58d4-450b-8e36-086b810cd9e5', + name: 'IFP Week', + }, + { + id: 'b4e072e6-38f8-4f47-bc91-f3e7ef55b3a0', + name: 'International Documentary Film Festival Amsterdam', + }, + { + id: '56c02ef9-8f59-4aee-aa36-70a69482807c', + name: 'International Film Festival Rotterdam', + }, + { + id: 'e69be77e-8128-4e29-a1aa-215b14e55633', + name: 'Karlovy Vary International Film Festival', + }, + { + id: '9719b9a4-6374-4a0d-8129-61c50951a271', + name: 'Los Angeles Film Festival', + }, + { + id: '9c550c61-46a7-4416-81b0-190f0e8e4126', + name: 'Montclair Film Festival', + }, + { + id: 'cefe282e-d860-4f63-83c2-276ee3cd3e51', + name: 'New Directors/New Films | MoMA', + }, + { + id: '8890449d-dae1-41f8-b163-5f228f03a8b1', + name: 'New York Film Festival', + }, + { + id: 'c9b59f0a-d4e2-4d76-b1a9-dcfb9c2c92c3', + name: 'Outfest', + }, + { + id: '2f8b79cc-11b0-4688-89b0-34d6fd5dfe4f', + name: 'Palm Springs International Film Festival', + }, + { + id: '9c5d9a17-c68a-4688-99b5-724a906b13e8', + name: 'Panafrican Film and Television Festival of Ouagadougou', + }, + { + id: '7864ff63-dc49-48d8-bf8b-72a82c4bc5e7', + name: 'Raindance Film Festival', + }, + { + id: '7f4fcd1e-79bb-4e02-9070-4c5b47ff360a', + name: 'Rooftop Films Summer Series', + }, + { + id: '5e3ae402-58a2-48b0-b505-63c32b3f7bb1', + name: 'San Francisco International Film Festival', + }, + { + id: 'f1c883a2-ec60-49df-96cb-ea77658de16c', + name: 'San Francisco Jewish Film Festival', + }, + { + id: '831f6f9e-c8d3-477d-9fd9-240f7b5b4641', + name: 'San Sebastian International Film Festival', + }, + { + id: 'f796b25e-07f3-4695-b3bb-5ee49764c917', + name: 'Sarajevo Film Festival', + }, + { + id: '932f8775-23b0-4f97-a1e3-545d7fca4ad8', + name: 'Sarasota Film Festival', + }, + { + id: '44f8c82a-3018-4f25-a5ab-d3106b5c4857', + name: 'Seattle International Film Festival', + }, + { + id: 'd28809c1-486a-4e89-9323-0ac7a73b1d0d', + name: 'Sedona Film Festival', + }, + { + id: '214fa7a5-2e8f-4c36-96a1-6e212e18a649', + name: 'Sheffield Doc/Fest', + }, + { + id: 'd0a4389e-e180-43eb-bf68-25d3f943b7f5', + name: 'Sitges Film Festival', + }, + { + id: 'd83d706d-34cb-4661-8850-e4e8c324f1f4', + name: 'Slamdance Film Festival', + }, + { + id: 'f3e6ad6a-4b78-441b-973d-46296d7a6c98', + name: 'Stanley Film Festival', + }, + { + id: '69782df3-5d12-4e6c-9963-c2f4e1342899', + name: 'Sundance Film Festival', + }, + { + id: 'c2c27cd3-8c43-4d51-9a22-05b0639448e7', + name: 'Sundance Film Festival New Frontiers', + }, + { + id: 'b925675b-3f4d-4d89-83f5-0e8f261f0d51', + name: 'Sundance Film Festival NEXT', + }, + { + id: '6d53d0eb-22fc-44e6-8ac2-4e66f9208f56', + name: 'Sundance Film Festival World Cinema', + }, + { + id: 'c83d04b7-296e-4d9b-8791-1093c41a0f92', + name: 'Sundance NEXT', + }, + { + id: 'caecb65a-38b7-4929-b0f1-f9b58e4239d8', + name: 'SXSW Film Festival', + }, + { + id: '61e86416-93af-4647-ba68-cb1d97d32e4c', + name: 'Telluride Film Festival', + }, + { + id: '6b9e1468-4e77-4f2e-b742-18c62a27e8c6', + name: 'Thessaloniki International Film Festival', + }, + { + id: 'b0a2c456-95de-449f-81df-e10e0262e250', + name: 'Toronto International Film Festival', + }, + { + id: 'b284e8d9-b186-4c54-9972-fa0f0ffae94d', + name: 'Tribeca Film Festival', + }, + { + id: 'b046b244-0b58-4a85-855f-2f7785ec4017', + name: 'True/False Documentary Film Festival', + }, + { + id: 'a6a4828a-10ae-44c4-b6c8-8a1c6b42a182', + name: 'Vancouver International Film Festival', + }, + { + id: '6e2be543-f055-4a28-b21e-7a2b1b3c0422', + name: 'Venice International Film Festival', + }, + { + id: '5c9d0ac7-61fc-4b7c-b9b3-ef1f95c04b4b', + name: 'Vienna International Film Festival', + }, + ]; + + await queryRunner.manager.save( + queryRunner.manager.create>( + AwardIssuer, + issuers, + ), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.delete(AwardIssuer, {}); + } +} diff --git a/backend/src/database/migrations/1707420468655-seed-genres.ts b/backend/src/database/migrations/1707420468655-seed-genres.ts new file mode 100644 index 0000000..6590aaf --- /dev/null +++ b/backend/src/database/migrations/1707420468655-seed-genres.ts @@ -0,0 +1,105 @@ +import { Genre } from 'src/genres/entities/genre.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeedGenres1707420468655 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const genres = [ + { + id: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Action', + }, + { + id: '9d8be5ab-b956-4571-9b4e-1e25fb8b6e44', // REMOVE + name: 'Adventure', + }, + { + id: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Comedy', + }, + { + id: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Crime', + }, + { + id: 'cc22046e-0b43-426c-9e70-b91ac6a601ba', // REMOVE + name: 'Experimental', + }, + { + id: 'ed2515e3-da9a-4291-82bf-4ebe06831407', + name: 'Western', + }, + { + id: '37306a11-e894-4e90-a200-87aabf67f70a', // REMOVE + name: 'Historical', + }, + { + id: 'b20d0c86-b2d4-4fb0-8b39-dc855591fa24', // REMOVE + name: 'Fiction', + }, + { + id: '8d7262a8-a94b-4f85-886a-a5ecff8844b3', + name: 'Mystery', + }, + { + id: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Documentary', + }, + { + id: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Drama', + }, + { + id: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Horror', + }, + { + id: '79a15574-5e83-4daf-8fdb-dd2261e12b60', + name: 'Romance', + }, + { + id: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Science Fiction', + }, + { + id: '004d484a-488e-478e-90f5-b37de3961527', + name: 'Fantasy', + }, + { + id: '281857e3-0772-4d7d-b5a8-e460dde75b91', + name: 'Thriller', + }, + { + id: '26703192-9875-456c-8bb6-6f7acaa8f0bf', // REMOVE + name: 'Noir', + }, + { + id: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Musical', + }, + { + id: '62804361-7304-4de7-ae4a-bc3c742ec26b', // REMOVE + name: 'War', + }, + { + id: '9568217b-affa-4859-a342-1ffeca0b7595', // REMOVE + name: 'Sports', + }, + { + id: '21d8c163-88cc-414d-a381-78660065019d', // REMOVE + name: 'Travel', + }, + { + id: '03342d8a-9fa0-4df1-9047-52c226b43b05', // REMOVE + name: 'Biopic', + }, + ]; + + await queryRunner.manager.save( + queryRunner.manager.create>(Genre, genres), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.delete(Genre, {}); + } +} diff --git a/backend/src/database/migrations/1708539423759-make-film-slug-unique.ts b/backend/src/database/migrations/1708539423759-make-film-slug-unique.ts new file mode 100644 index 0000000..aca3132 --- /dev/null +++ b/backend/src/database/migrations/1708539423759-make-film-slug-unique.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class makeFilmSlugUnique1708539423759 implements MigrationInterface { + name = ' makeFilmSlugUnique1708539423759'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" ADD CONSTRAINT "UQ_5be832e482e546ce0ae2ead2b31" UNIQUE ("slug")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "films" DROP CONSTRAINT "UQ_5be832e482e546ce0ae2ead2b31"`, + ); + } +} diff --git a/backend/src/database/migrations/1709299646834-genres-modifications.ts b/backend/src/database/migrations/1709299646834-genres-modifications.ts new file mode 100644 index 0000000..a605cd3 --- /dev/null +++ b/backend/src/database/migrations/1709299646834-genres-modifications.ts @@ -0,0 +1,54 @@ +import { Genre } from 'src/genres/entities/genre.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class GenresModifications1709299646834 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const newGenres = [ + { + id: 'bfb5f952-8dd9-45f4-bb27-530fa728250c', + name: 'Dance', + }, + ]; + + const deleteGenres = [ + { + id: 'b20d0c86-b2d4-4fb0-8b39-dc855591fa24', + name: 'Fiction', + }, + ]; + + await queryRunner.manager.save( + queryRunner.manager.create>( + Genre, + newGenres, + ), + ); + + await queryRunner.manager.delete(Genre, deleteGenres); + } + + public async down(queryRunner: QueryRunner): Promise { + const newGenres = [ + { + id: 'bfb5f952-8dd9-45f4-bb27-530fa728250c', + name: 'Dance', + }, + ]; + + const deleteGenres = [ + { + id: 'b20d0c86-b2d4-4fb0-8b39-dc855591fa24', + name: 'Fiction', + }, + ]; + + await queryRunner.manager.delete(Genre, newGenres); + + await queryRunner.manager.save( + queryRunner.manager.create>( + Genre, + deleteGenres, + ), + ); + } +} diff --git a/backend/src/database/migrations/1711463567866-rent-table.ts b/backend/src/database/migrations/1711463567866-rent-table.ts new file mode 100644 index 0000000..46f81e4 --- /dev/null +++ b/backend/src/database/migrations/1711463567866-rent-table.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RentTable1711463567866 implements MigrationInterface { + name = 'RentTable1711463567866'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "rents" ("id" character varying NOT NULL, "film_id" character varying NOT NULL, "user_id" character varying NOT NULL, "user_paid" integer NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_585ff08e8abeed412d0844db978" PRIMARY KEY ("id", "film_id", "user_id"))`, + ); + await queryRunner.query( + `ALTER TABLE "payments" ADD "type" character varying NOT NULL DEFAULT 'watch'`, + ); + await queryRunner.query(`ALTER TABLE "films" ADD "rental_price" integer`); + await queryRunner.query( + `ALTER TABLE "rents" ADD CONSTRAINT "FK_8c8a9ad8d81367d23eb84c1bca8" FOREIGN KEY ("film_id") REFERENCES "films"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "rents" ADD CONSTRAINT "FK_123a88669a1dd17e9b3cea64cac" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rents" DROP CONSTRAINT "FK_123a88669a1dd17e9b3cea64cac"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" DROP CONSTRAINT "FK_8c8a9ad8d81367d23eb84c1bca8"`, + ); + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "rental_price"`); + await queryRunner.query(`ALTER TABLE "payments" DROP COLUMN "type"`); + await queryRunner.query(`DROP TABLE "rents"`); + } +} diff --git a/backend/src/database/migrations/1711472504069-rental-price-decimal.ts b/backend/src/database/migrations/1711472504069-rental-price-decimal.ts new file mode 100644 index 0000000..3ecf16a --- /dev/null +++ b/backend/src/database/migrations/1711472504069-rental-price-decimal.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RentalPriceDecimal1711472504069 implements MigrationInterface { + name = 'RentalPriceDecimal1711472504069'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "rental_price"`); + await queryRunner.query( + `ALTER TABLE "films" ADD "rental_price" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" DROP COLUMN "rental_price"`); + await queryRunner.query(`ALTER TABLE "films" ADD "rental_price" integer`); + } +} diff --git a/backend/src/database/migrations/1711473065336-slug-index.ts b/backend/src/database/migrations/1711473065336-slug-index.ts new file mode 100644 index 0000000..0f9eb55 --- /dev/null +++ b/backend/src/database/migrations/1711473065336-slug-index.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SlugIndex1711473065336 implements MigrationInterface { + name = 'SlugIndex1711473065336'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX "IDX_5be832e482e546ce0ae2ead2b3" ON "films" ("slug") `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "public"."IDX_5be832e482e546ce0ae2ead2b3"`, + ); + } +} diff --git a/backend/src/database/migrations/1711564071965-updated-rent-table.ts b/backend/src/database/migrations/1711564071965-updated-rent-table.ts new file mode 100644 index 0000000..28f6fe8 --- /dev/null +++ b/backend/src/database/migrations/1711564071965-updated-rent-table.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdatedRentTable1711564071965 implements MigrationInterface { + name = 'UpdatedRentTable1711564071965'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "rents" DROP COLUMN "user_paid"`); + await queryRunner.query( + `ALTER TABLE "rents" ADD "usd_amount" integer NOT NULL DEFAULT '0'`, + ); + await queryRunner.query( + `ALTER TABLE "rents" ADD "status" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "rents" ADD "provider_id" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "rents" DROP COLUMN "provider_id"`); + await queryRunner.query(`ALTER TABLE "rents" DROP COLUMN "status"`); + await queryRunner.query(`ALTER TABLE "rents" DROP COLUMN "usd_amount"`); + await queryRunner.query( + `ALTER TABLE "rents" ADD "user_paid" integer NOT NULL DEFAULT '0'`, + ); + } +} diff --git a/backend/src/database/migrations/1711564180593-updated-usd-amount.ts b/backend/src/database/migrations/1711564180593-updated-usd-amount.ts new file mode 100644 index 0000000..48630fb --- /dev/null +++ b/backend/src/database/migrations/1711564180593-updated-usd-amount.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdatedUsdAmount1711564180593 implements MigrationInterface { + name = 'UpdatedUsdAmount1711564180593'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "rents" DROP COLUMN "usd_amount"`); + await queryRunner.query( + `ALTER TABLE "rents" ADD "usd_amount" numeric(5,2) NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "rents" DROP COLUMN "usd_amount"`); + await queryRunner.query( + `ALTER TABLE "rents" ADD "usd_amount" integer NOT NULL DEFAULT '0'`, + ); + } +} diff --git a/backend/src/database/migrations/1711985810103-added-rent-revenue.ts b/backend/src/database/migrations/1711985810103-added-rent-revenue.ts new file mode 100644 index 0000000..7cdfe4d --- /dev/null +++ b/backend/src/database/migrations/1711985810103-added-rent-revenue.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedRentRevenue1711985810103 implements MigrationInterface { + name = 'AddedRentRevenue1711985810103'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ADD "rent_pending_revenue" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP COLUMN "rent_pending_revenue"`, + ); + } +} diff --git a/backend/src/database/migrations/1712074358461-added-scale-pending-revenue.ts b/backend/src/database/migrations/1712074358461-added-scale-pending-revenue.ts new file mode 100644 index 0000000..c74f837 --- /dev/null +++ b/backend/src/database/migrations/1712074358461-added-scale-pending-revenue.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedScalePendingRevenue1712074358461 + implements MigrationInterface +{ + name = 'AddedScalePendingRevenue1712074358461'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "pending_revenue" TYPE numeric(10,10)`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "rent_pending_revenue" TYPE numeric(10,10)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "rent_pending_revenue" TYPE numeric(10,2)`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "pending_revenue" TYPE numeric(10,2)`, + ); + } +} diff --git a/backend/src/database/migrations/1712782114297-added-invite-permissions.ts b/backend/src/database/migrations/1712782114297-added-invite-permissions.ts new file mode 100644 index 0000000..1b1b8e9 --- /dev/null +++ b/backend/src/database/migrations/1712782114297-added-invite-permissions.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedInvitePermissions1712782114297 implements MigrationInterface { + name = 'AddedInvitePermissions1712782114297'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "permissions" ADD "email" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "PK_568e9d106e96fbcb887c10b334c"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "PK_b00195aed8f22173605716ebfa1" PRIMARY KEY ("id", "film_id")`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_287711659c1379214f4e851d8be"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ALTER COLUMN "filmmaker_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_287711659c1379214f4e851d8be" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_287711659c1379214f4e851d8be"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "PK_b00195aed8f22173605716ebfa1"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "PK_568e9d106e96fbcb887c10b334c" PRIMARY KEY ("id", "filmmaker_id", "film_id")`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ALTER COLUMN "filmmaker_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_287711659c1379214f4e851d8be" FOREIGN KEY ("filmmaker_id") REFERENCES "filmmakers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query(`ALTER TABLE "permissions" DROP COLUMN "email"`); + } +} diff --git a/backend/src/database/migrations/1712782114297-fix-precision-in-shareholder.ts b/backend/src/database/migrations/1712782114297-fix-precision-in-shareholder.ts new file mode 100644 index 0000000..a068654 --- /dev/null +++ b/backend/src/database/migrations/1712782114297-fix-precision-in-shareholder.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixPrecisionInShareholder1712782114297 + implements MigrationInterface +{ + name = 'FixPrecisionInShareholder1712782114297'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "pending_revenue" TYPE numeric(20,10)`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "rent_pending_revenue" TYPE numeric(20,10)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "rent_pending_revenue" TYPE numeric(10,10)`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "pending_revenue" TYPE numeric(10,10)`, + ); + } +} diff --git a/backend/src/database/migrations/1712849458827-rename-films-table.ts b/backend/src/database/migrations/1712849458827-rename-films-table.ts new file mode 100644 index 0000000..04b4283 --- /dev/null +++ b/backend/src/database/migrations/1712849458827-rename-films-table.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameFilmsTable1712849458827 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "films" RENAME TO "projects"`); + await queryRunner.query( + `CREATE TABLE "contents" ("id" character varying NOT NULL, "project_id" character varying NOT NULL, "title" character varying, "synopsis" character varying, "file" character varying, "season" integer, "order" integer NOT NULL DEFAULT '1', "rental_price" numeric(10,2) NOT NULL DEFAULT '0', "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_b7c504072e537532d7080c54fac" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" RENAME TO "films"`); + await queryRunner.query(`DROP TABLE "contents"`); + } +} diff --git a/backend/src/database/migrations/1712851782534-rename-films-genre-table.ts b/backend/src/database/migrations/1712851782534-rename-films-genre-table.ts new file mode 100644 index 0000000..b397112 --- /dev/null +++ b/backend/src/database/migrations/1712851782534-rename-films-genre-table.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class renameFilmsGenreTable1712851782534 implements MigrationInterface { + name = 'renameFilmsGenreTable1712851782534'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "film_genres" RENAME TO "project_genres"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "project_genres" RENAME TO "film_genres"`, + ); + } +} diff --git a/backend/src/database/migrations/1712854063536-update-relations.ts b/backend/src/database/migrations/1712854063536-update-relations.ts new file mode 100644 index 0000000..476f1b6 --- /dev/null +++ b/backend/src/database/migrations/1712854063536-update-relations.ts @@ -0,0 +1,233 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateRelations1712854063536 implements MigrationInterface { + name = 'UpdateRelations1712854063536'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "invites" DROP CONSTRAINT "FK_90b37b2a69026d59f882680da11"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" DROP CONSTRAINT "FK_0c808b1f5954f34ba80e18e934c"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" DROP CONSTRAINT "FK_fcd6379cdf6bad96ad5bed0e8d2"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" DROP CONSTRAINT "FK_f9bd206872ffb6401f9992eef3b"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" DROP CONSTRAINT "FK_b72da3cfc0781b8e8bf0ec07b55"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" DROP CONSTRAINT "FK_97a23ecdb3849289ba88204317f"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" DROP CONSTRAINT "FK_9b6ca3be1e09e7537b24144d21d"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" DROP CONSTRAINT "FK_d273e8fd854a03f655a751f393e"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_29e02d061b7c91002165230d636"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" DROP CONSTRAINT "FK_8c8a9ad8d81367d23eb84c1bca8"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_8f8a4ee33f8afe7b90bc6b4c0a6"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_5be832e482e546ce0ae2ead2b3"`, + ); + await queryRunner.query( + `ALTER TABLE "invites" RENAME COLUMN "film_id" TO "content_id"`, + ); + await queryRunner.query( + `ALTER TABLE "invites" RENAME CONSTRAINT "PK_79bbf0f768df5e816e421b4c050" TO "PK_1ae5ed79d5c1b565223e3604fb3"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" RENAME COLUMN "film_id" TO "content_id"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" RENAME CONSTRAINT "PK_481951f875efb1e4e07812e25c5" TO "PK_cda97899daf3577efed764dcf08"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" RENAME COLUMN "film_id" TO "content_id"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" RENAME CONSTRAINT "PK_82339ba2ac2655387277341883f" TO "PK_4d3a365691a54b82f542add5e66"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" RENAME COLUMN "film_id" TO "project_id"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" RENAME CONSTRAINT "PK_2517c15a5bc76c5055f9b909261" TO "PK_3a391e09f4d82c9713513eb5c36"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" RENAME COLUMN "film_id" TO "project_id"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" RENAME CONSTRAINT "PK_cfec9a37e1774cd1c77c0e78d93" TO "PK_e29f6cff0ecb23d7802b0beaaa7"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" RENAME COLUMN "film_id" TO "project_id"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" RENAME CONSTRAINT "PK_c6952b0a27da64ed67e193ca1f3" TO "PK_3c97877d1d262a6ffdfefe48d9a"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" RENAME COLUMN "film_id" TO "project_id"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" RENAME COLUMN "film_id" TO "project_id"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" RENAME CONSTRAINT "PK_b00195aed8f22173605716ebfa1" TO "PK_80900b43db56738c6fde4cd60d3"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" RENAME COLUMN "film_id" TO "content_id"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" RENAME CONSTRAINT "PK_585ff08e8abeed412d0844db978" TO "PK_3e9ea9a200e8ad860639f6fd4dc"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" RENAME COLUMN "film_id" TO "content_id"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" ADD CONSTRAINT "FK_aa65e23c7838dd942f18494c4ee" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" ADD CONSTRAINT "FK_3bc09d27d7148e0adc75280e61d" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "documents" ADD CONSTRAINT "FK_e156b298c20873e14c362e789bf" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD CONSTRAINT "FK_e4484a8f40c2c996bb65e10f556" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD CONSTRAINT "FK_44a67d9910f540fff30cfb44529" FOREIGN KEY ("genre_id") REFERENCES "genres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_498404ce35391987477a8e10c21" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "contents" ADD CONSTRAINT "FK_d873881337a2c2f7dc561d7d009" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" DROP CONSTRAINT "FK_d873881337a2c2f7dc561d7d009"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_498404ce35391987477a8e10c21"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" DROP CONSTRAINT "FK_44a67d9910f540fff30cfb44529"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" DROP CONSTRAINT "FK_e4484a8f40c2c996bb65e10f556"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" DROP CONSTRAINT "FK_e156b298c20873e14c362e789bf"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" DROP CONSTRAINT "FK_3bc09d27d7148e0adc75280e61d"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" DROP CONSTRAINT "FK_aa65e23c7838dd942f18494c4ee"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" RENAME COLUMN "content_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" RENAME CONSTRAINT "PK_3e9ea9a200e8ad860639f6fd4dc" TO "PK_585ff08e8abeed412d0844db978"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" RENAME COLUMN "content_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" RENAME CONSTRAINT "PK_80900b43db56738c6fde4cd60d3" TO "PK_b00195aed8f22173605716ebfa1"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" RENAME COLUMN "project_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" RENAME COLUMN "project_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" RENAME CONSTRAINT "PK_3c97877d1d262a6ffdfefe48d9a" TO "PK_c6952b0a27da64ed67e193ca1f3"`, + ); + await queryRunner.query( + `ALTER TABLE "documents" RENAME COLUMN "project_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" RENAME CONSTRAINT "PK_e29f6cff0ecb23d7802b0beaaa7" TO "PK_cfec9a37e1774cd1c77c0e78d93"`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" RENAME COLUMN "project_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" RENAME CONSTRAINT "PK_3a391e09f4d82c9713513eb5c36" TO "PK_2517c15a5bc76c5055f9b909261"`, + ); + await queryRunner.query( + `ALTER TABLE "awards" RENAME COLUMN "project_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" RENAME CONSTRAINT "PK_4d3a365691a54b82f542add5e66" TO "PK_82339ba2ac2655387277341883f"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" RENAME COLUMN "content_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" RENAME CONSTRAINT "PK_cda97899daf3577efed764dcf08" TO "PK_481951f875efb1e4e07812e25c5"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" RENAME COLUMN "content_id" TO "film_id"`, + ); + await queryRunner.query( + `ALTER TABLE "invites" RENAME CONSTRAINT "PK_1ae5ed79d5c1b565223e3604fb3" TO "PK_79bbf0f768df5e816e421b4c050"`, + ); + await queryRunner.query( + `ALTER TABLE "invites" RENAME COLUMN "content_id" TO "film_id"`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_5be832e482e546ce0ae2ead2b3" ON "projects" ("slug") `, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_8f8a4ee33f8afe7b90bc6b4c0a6" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "rents" ADD CONSTRAINT "FK_8c8a9ad8d81367d23eb84c1bca8" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_29e02d061b7c91002165230d636" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD CONSTRAINT "FK_d273e8fd854a03f655a751f393e" FOREIGN KEY ("genre_id") REFERENCES "genres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD CONSTRAINT "FK_9b6ca3be1e09e7537b24144d21d" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "documents" ADD CONSTRAINT "FK_97a23ecdb3849289ba88204317f" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "festival_screenings" ADD CONSTRAINT "FK_b72da3cfc0781b8e8bf0ec07b55" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "awards" ADD CONSTRAINT "FK_f9bd206872ffb6401f9992eef3b" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD CONSTRAINT "FK_fcd6379cdf6bad96ad5bed0e8d2" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD CONSTRAINT "FK_0c808b1f5954f34ba80e18e934c" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "invites" ADD CONSTRAINT "FK_90b37b2a69026d59f882680da11" FOREIGN KEY ("film_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1712854063537-add-constraints.ts b/backend/src/database/migrations/1712854063537-add-constraints.ts new file mode 100644 index 0000000..99a4335 --- /dev/null +++ b/backend/src/database/migrations/1712854063537-add-constraints.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddConstraints1712854063537 implements MigrationInterface { + name = 'AddConstraints1712854063537'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rents" ADD CONSTRAINT "FK_2f265faa6b66d4af9912157bf38" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "invites" ADD CONSTRAINT "FK_3e0cd070e56254fc7afbd921cb1" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "casts" ADD CONSTRAINT "FK_4690db3a3b4ed04ddaab14c8c87" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "crews" ADD CONSTRAINT "FK_d180f228df7b5f8d33a1fb45318" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "invites" DROP CONSTRAINT "FK_3e0cd070e56254fc7afbd921cb1"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "rents" DROP CONSTRAINT "FK_2f265faa6b66d4af9912157bf38"`, + ); + await queryRunner.query( + `ALTER TABLE "casts" DROP CONSTRAINT "FK_4690db3a3b4ed04ddaab14c8c87"`, + ); + await queryRunner.query( + `ALTER TABLE "crews" DROP CONSTRAINT "FK_d180f228df7b5f8d33a1fb45318"`, + ); + } +} diff --git a/backend/src/database/migrations/1713902357490-rename-type-to-category.ts b/backend/src/database/migrations/1713902357490-rename-type-to-category.ts new file mode 100644 index 0000000..7d8fe38 --- /dev/null +++ b/backend/src/database/migrations/1713902357490-rename-type-to-category.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameTypeToCategory1713902357490 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + queryRunner.query(`ALTER TABLE projects RENAME COLUMN type TO category`); + } + + public async down(queryRunner: QueryRunner): Promise { + queryRunner.query(`ALTER TABLE projects RENAME COLUMN category TO type`); + } +} diff --git a/backend/src/database/migrations/1713902464661-add-type-column.ts b/backend/src/database/migrations/1713902464661-add-type-column.ts new file mode 100644 index 0000000..c3021df --- /dev/null +++ b/backend/src/database/migrations/1713902464661-add-type-column.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTypeColumn1713902464661 implements MigrationInterface { + name = 'AddTypeColumn1713902464661'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "projects" ADD "type" character varying NOT NULL DEFAULT 'film'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "type"`); + } +} diff --git a/backend/src/database/migrations/1715106393903-added-captions.ts b/backend/src/database/migrations/1715106393903-added-captions.ts new file mode 100644 index 0000000..d154fa7 --- /dev/null +++ b/backend/src/database/migrations/1715106393903-added-captions.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedCaptions1715106393903 implements MigrationInterface { + name = 'AddedCaptions1715106393903'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "captions" ("id" character varying NOT NULL, "content_id" character varying NOT NULL, "language" character varying NOT NULL, "url" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_730a1b3634f829a306a8ba76abc" PRIMARY KEY ("id", "content_id"))`, + ); + await queryRunner.query( + `ALTER TABLE "captions" ADD CONSTRAINT "FK_47a387903be71257f14c8b22052" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "captions" DROP CONSTRAINT "FK_47a387903be71257f14c8b22052"`, + ); + await queryRunner.query(`DROP TABLE "captions"`); + } +} diff --git a/backend/src/database/migrations/1715106393904-add-events-column-to-subscription.ts b/backend/src/database/migrations/1715106393904-add-events-column-to-subscription.ts new file mode 100644 index 0000000..a7699a9 --- /dev/null +++ b/backend/src/database/migrations/1715106393904-add-events-column-to-subscription.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddEventColumnToSubscription1715106393904 + implements MigrationInterface +{ + name = 'AddEventColumnToSubscription1715106393904'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" ADD "flash_events" jsonb`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" DROP COLUMN "flash_events"`, + ); + } +} diff --git a/backend/src/database/migrations/1717527985895-add-status-column.ts b/backend/src/database/migrations/1717527985895-add-status-column.ts new file mode 100644 index 0000000..e38bc86 --- /dev/null +++ b/backend/src/database/migrations/1717527985895-add-status-column.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddStatusColumn1717527985895 implements MigrationInterface { + name = 'AddStatusColumn1717527985895'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" ADD "status" character varying NOT NULL DEFAULT 'processing'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "contents" DROP COLUMN "status"`); + } +} diff --git a/backend/src/database/migrations/1717622169971-renamed-name.ts b/backend/src/database/migrations/1717622169971-renamed-name.ts new file mode 100644 index 0000000..114b9e5 --- /dev/null +++ b/backend/src/database/migrations/1717622169971-renamed-name.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenamedName1717622169971 implements MigrationInterface { + name = 'RenamedName1717622169971'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "projects" RENAME COLUMN "project_name" TO "name"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "projects" RENAME COLUMN "name" TO "project_name"`, + ); + } +} diff --git a/backend/src/database/migrations/1717701228354-added-content-metadata.ts b/backend/src/database/migrations/1717701228354-added-content-metadata.ts new file mode 100644 index 0000000..f3c5e37 --- /dev/null +++ b/backend/src/database/migrations/1717701228354-added-content-metadata.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedContentMetadata1717701228354 implements MigrationInterface { + name = 'AddedContentMetadata1717701228354'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "file"`); + await queryRunner.query(`ALTER TABLE "contents" ADD "metadata" json`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "contents" DROP COLUMN "metadata"`); + await queryRunner.query( + `ALTER TABLE "projects" ADD "file" character varying`, + ); + } +} diff --git a/backend/src/database/migrations/1718892002782-added-rss-shareholders.ts b/backend/src/database/migrations/1718892002782-added-rss-shareholders.ts new file mode 100644 index 0000000..10f4498 --- /dev/null +++ b/backend/src/database/migrations/1718892002782-added-rss-shareholders.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedRssShareholders1718892002782 implements MigrationInterface { + name = 'AddedRssShareholders1718892002782'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "rss_shareholders" ("id" character varying NOT NULL, "content_id" character varying NOT NULL, "lightning_address" character varying NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_56a991a9ee87dcbb8ef414d349b" PRIMARY KEY ("id", "content_id"))`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ADD CONSTRAINT "FK_ac33e69f2eebff4cdba59bf4f86" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rss_shareholders" DROP CONSTRAINT "FK_ac33e69f2eebff4cdba59bf4f86"`, + ); + await queryRunner.query(`DROP TABLE "rss_shareholders"`); + } +} diff --git a/backend/src/database/migrations/1718909804031-added-shares-to-rss.ts b/backend/src/database/migrations/1718909804031-added-shares-to-rss.ts new file mode 100644 index 0000000..c6e0694 --- /dev/null +++ b/backend/src/database/migrations/1718909804031-added-shares-to-rss.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedSharesToRss1718909804031 implements MigrationInterface { + name = 'AddedSharesToRss1718909804031'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ADD "share" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rss_shareholders" DROP COLUMN "share"`, + ); + } +} diff --git a/backend/src/database/migrations/1719501880840-added-custom-wallet-info.ts b/backend/src/database/migrations/1719501880840-added-custom-wallet-info.ts new file mode 100644 index 0000000..259140a --- /dev/null +++ b/backend/src/database/migrations/1719501880840-added-custom-wallet-info.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedCustomWalletInfo1719501880840 implements MigrationInterface { + name = 'AddedCustomWalletInfo1719501880840'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ADD "name" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ADD "node_public_key" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ADD "key" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ADD "value" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ALTER COLUMN "lightning_address" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "rss_shareholders" ALTER COLUMN "lightning_address" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" DROP COLUMN "value"`, + ); + await queryRunner.query(`ALTER TABLE "rss_shareholders" DROP COLUMN "key"`); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" DROP COLUMN "node_public_key"`, + ); + await queryRunner.query( + `ALTER TABLE "rss_shareholders" DROP COLUMN "name"`, + ); + } +} diff --git a/backend/src/database/migrations/1722433495278-added-is-rss-enabled.ts b/backend/src/database/migrations/1722433495278-added-is-rss-enabled.ts new file mode 100644 index 0000000..33c7463 --- /dev/null +++ b/backend/src/database/migrations/1722433495278-added-is-rss-enabled.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedIsRssEnabled1722433495278 implements MigrationInterface { + name = 'AddedIsRssEnabled1722433495278'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" ADD "is_rss_enabled" boolean NOT NULL DEFAULT false`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" DROP COLUMN "is_rss_enabled"`, + ); + } +} diff --git a/backend/src/database/migrations/1725982864151-rename-pro-to-pro-plus.ts b/backend/src/database/migrations/1725982864151-rename-pro-to-pro-plus.ts new file mode 100644 index 0000000..6d2d950 --- /dev/null +++ b/backend/src/database/migrations/1725982864151-rename-pro-to-pro-plus.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameProToProPlus1725982864151 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // update the rows that have the value 'pro+' to 'pro-plus' in the subscriptions table for the column type + await queryRunner.query( + `UPDATE subscriptions SET type = 'pro-plus' WHERE type = 'pro+'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // update the rows that have the value 'pro-plus' to 'pro+' in the subscriptions table for the column type + await queryRunner.query( + `UPDATE subscriptions SET type = 'pro+' WHERE type = 'pro-plus'`, + ); + } +} diff --git a/backend/src/database/migrations/1729096262567-music-videos-update.ts b/backend/src/database/migrations/1729096262567-music-videos-update.ts new file mode 100644 index 0000000..d0fda8f --- /dev/null +++ b/backend/src/database/migrations/1729096262567-music-videos-update.ts @@ -0,0 +1,32 @@ +/* eslint-disable unicorn/no-array-method-this-argument */ +import { Project } from 'src/projects/entities/project.entity'; +import { Category } from 'src/projects/enums/category.enum'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MusicVideosUpdate1729096262567 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // update all projects that have category music-video to have the category narrative and type music-video + const projects = await queryRunner.manager.find(Project, { + where: { category: 'music-video' as Category }, + }); + const updatedProjects = projects.map((project) => { + project.category = 'narrative'; + project.type = 'music-video'; + return project; + }); + await queryRunner.manager.save(updatedProjects); + } + + public async down(queryRunner: QueryRunner): Promise { + // update all projects that have category narrative and type music-video to have the category music-video + const projects = await queryRunner.manager.find(Project, { + where: { category: 'narrative' as Category, type: 'music-video' }, + }); + const updatedProjects = projects.map((project) => { + project.category = 'music-video' as Category; + project.type = 'film'; + return project; + }); + await queryRunner.manager.save(updatedProjects); + } +} diff --git a/backend/src/database/migrations/1730125761447-added-subgenre-table.ts b/backend/src/database/migrations/1730125761447-added-subgenre-table.ts new file mode 100644 index 0000000..155f1b8 --- /dev/null +++ b/backend/src/database/migrations/1730125761447-added-subgenre-table.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedSubgenreTable1730125761447 implements MigrationInterface { + name = 'AddedSubgenreTable1730125761447'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "subgenres" ("id" character varying NOT NULL, "name" character varying NOT NULL, "genre_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5532e81efee4754bdf3eaefcc2a" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "projects" ADD "rejected_reason" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "subgenres" ADD CONSTRAINT "FK_4b4235847b2ea817e2ab1d40d52" FOREIGN KEY ("genre_id") REFERENCES "genres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subgenres" DROP CONSTRAINT "FK_4b4235847b2ea817e2ab1d40d52"`, + ); + await queryRunner.query( + `ALTER TABLE "projects" DROP COLUMN "rejected_reason"`, + ); + await queryRunner.query(`DROP TABLE "subgenres"`); + } +} diff --git a/backend/src/database/migrations/1730125784613-seed-subgenres.ts b/backend/src/database/migrations/1730125784613-seed-subgenres.ts new file mode 100644 index 0000000..10c2f32 --- /dev/null +++ b/backend/src/database/migrations/1730125784613-seed-subgenres.ts @@ -0,0 +1,485 @@ +import { randomUUID } from 'node:crypto'; +import { In, MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeedSubgenres1730125784613 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // 1. Clean up the projects that have old genres + // 2. Clean up the old genres + // 3. Seed the new genres + // 4. Seed the new subgenres + // 5. Update the projects with the new genres + + const genresToRemove = [ + '9d8be5ab-b956-4571-9b4e-1e25fb8b6e44', + 'cc22046e-0b43-426c-9e70-b91ac6a601ba', + '37306a11-e894-4e90-a200-87aabf67f70a', + 'b20d0c86-b2d4-4fb0-8b39-dc855591fa24', + '26703192-9875-456c-8bb6-6f7acaa8f0bf', + '62804361-7304-4de7-ae4a-bc3c742ec26b', + '9568217b-affa-4859-a342-1ffeca0b7595', + '21d8c163-88cc-414d-a381-78660065019d', + '03342d8a-9fa0-4df1-9047-52c226b43b05', + ]; + + await queryRunner.manager.delete('project_genres', { + genre_id: In(genresToRemove), + }); + + await queryRunner.manager.delete('genres', { + id: In(genresToRemove), + }); + + const newGenres = [ + { + id: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'Animation', + }, + ]; + + await queryRunner.manager.save('genres', newGenres); + + const subgenres = [ + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Adventure', + }, + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Disaster', + }, + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Martial Arts', + }, + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Military Action', + }, + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Spy/Espionage', + }, + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Superhero', + }, + { + id: randomUUID(), + genreId: '3676ab4e-1b4f-4568-836a-43f84b1a6228', + name: 'Video Game Movies', + }, + { + id: randomUUID(), + genreId: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'CGI', + }, + { + id: randomUUID(), + genreId: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'Claymation/Stop Motion', + }, + { + id: randomUUID(), + genreId: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'Traditional Drawn', + }, + { + id: randomUUID(), + genreId: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'Cutout Animation', + }, + { + id: randomUUID(), + genreId: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'Live-Action Hybrid', + }, + { + id: randomUUID(), + genreId: '03342d8a-9fa0-4df1-9047-52c226b43b05', + name: 'Puppet Animation', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Black/Dark Comedy', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Buddy Comedy', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Hangout Movies', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Parody/Spoof', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Prank Movies', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Satire', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Slapstick', + }, + { + id: randomUUID(), + genreId: 'b7bac6dd-2a86-4191-ac1e-c92be7b8b841', + name: 'Screwball', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Cop Movies', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Crime Drama', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Crime Thriller', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Detective/Whodunnit', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Gangster Films', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Hardboiled', + }, + { + id: randomUUID(), + genreId: 'e9e4285c-b6f3-46ef-a991-bd9f1537eae1', + name: 'Heist/Caper', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Expository', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Observational', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Poetic', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Participatory', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Historical', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Reflexive', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Nature/Wildlife', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Social Issue', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Biographical', + }, + { + id: randomUUID(), + genreId: '51e56f26-bbb4-44e8-ac01-d7aa8ef81f3b', + name: 'Performative', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Docudrama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Melodrama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Teen Drama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Medical Drama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Legal Drama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Religious Drama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Sports Drama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Political Drama', + }, + { + id: randomUUID(), + genreId: '01854d65-636e-476b-9bbd-881c1834cdb0', + name: 'Philosophical Drama', + }, + { + id: randomUUID(), + genreId: '004d484a-488e-478e-90f5-b37de3961527', + name: 'Contemporary/Urban', + }, + { + id: randomUUID(), + genreId: '004d484a-488e-478e-90f5-b37de3961527', + name: 'Epic Fantasy', + }, + { + id: randomUUID(), + genreId: '004d484a-488e-478e-90f5-b37de3961527', + name: 'Fairy Tale', + }, + { + id: randomUUID(), + genreId: '004d484a-488e-478e-90f5-b37de3961527', + name: 'Dark Fantasy', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Ghost', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Zombie', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Werewolf', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Vampire', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Monster', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Slasher', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Body Horror', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Folk Horror', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Occult', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Found Footage', + }, + { + id: randomUUID(), + genreId: '0bc7d835-ed31-4a6f-a76f-cfe26595eb11', + name: 'Outbreak', + }, + { + id: randomUUID(), + genreId: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Broadway Adaptations', + }, + { + id: randomUUID(), + genreId: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Original Movie Musicals', + }, + { + id: randomUUID(), + genreId: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Jukebox Musicals', + }, + { + id: randomUUID(), + genreId: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Rock Operas', + }, + { + id: randomUUID(), + genreId: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Dance Movies', + }, + { + id: randomUUID(), + genreId: 'f99f1239-2d6c-45e5-87ef-ea1df6a223dc', + name: 'Concert Films', + }, + { + id: randomUUID(), + genreId: '79a15574-5e83-4daf-8fdb-dd2261e12b60', + name: 'Historical Romance', + }, + { + id: randomUUID(), + genreId: '79a15574-5e83-4daf-8fdb-dd2261e12b60', + name: 'Regency Romance', + }, + { + id: randomUUID(), + genreId: '79a15574-5e83-4daf-8fdb-dd2261e12b60', + name: 'Romantic Drama', + }, + { + id: randomUUID(), + genreId: '79a15574-5e83-4daf-8fdb-dd2261e12b60', + name: 'Romantic Comedy', + }, + { + id: randomUUID(), + genreId: '79a15574-5e83-4daf-8fdb-dd2261e12b60', + name: 'Fantasy Romance', + }, + { + id: randomUUID(), + genreId: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Space Opera', + }, + { + id: randomUUID(), + genreId: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Utopia', + }, + { + id: randomUUID(), + genreId: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Dystopia', + }, + { + id: randomUUID(), + genreId: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Contemporary Sci-Fi', + }, + { + id: randomUUID(), + genreId: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Cyberpunk', + }, + { + id: randomUUID(), + genreId: '0029a12a-e9b9-421f-90c0-446c61afb4dd', + name: 'Steampunk', + }, + { + id: randomUUID(), + genreId: '281857e3-0772-4d7d-b5a8-e460dde75b91', + name: 'Psychological', + }, + { + id: randomUUID(), + genreId: '281857e3-0772-4d7d-b5a8-e460dde75b91', + name: 'Mystery', + }, + { + id: randomUUID(), + genreId: '281857e3-0772-4d7d-b5a8-e460dde75b91', + name: 'Film Noir', + }, + { + id: randomUUID(), + genreId: '281857e3-0772-4d7d-b5a8-e460dde75b91', + name: 'Neo-noir', + }, + { + id: randomUUID(), + genreId: 'ed2515e3-da9a-4291-82bf-4ebe06831407', + name: 'Western', + }, + { + id: randomUUID(), + genreId: 'ed2515e3-da9a-4291-82bf-4ebe06831407', + name: 'Classic Western', + }, + { + id: randomUUID(), + genreId: 'ed2515e3-da9a-4291-82bf-4ebe06831407', + name: 'Spaghetti Western', + }, + { + id: randomUUID(), + genreId: 'ed2515e3-da9a-4291-82bf-4ebe06831407', + name: 'Modern Western', + }, + ]; + + await queryRunner.manager.save('subgenres', subgenres); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.delete('subgenres', {}); + } +} diff --git a/backend/src/database/migrations/1730337010575-added-type-column.ts b/backend/src/database/migrations/1730337010575-added-type-column.ts new file mode 100644 index 0000000..4cc8efc --- /dev/null +++ b/backend/src/database/migrations/1730337010575-added-type-column.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedTypeColumn1730337010575 implements MigrationInterface { + name = 'AddedTypeColumn1730337010575'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "project_genres" DROP CONSTRAINT "FK_44a67d9910f540fff30cfb44529"`, + ); + await queryRunner.query( + `ALTER TABLE "genres" ADD "type" character varying NOT NULL DEFAULT 'film'`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD "subgenre_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ALTER COLUMN "genre_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD CONSTRAINT "FK_cd4eb233bf2c800ec2621945efe" FOREIGN KEY ("subgenre_id") REFERENCES "subgenres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "project_genres" DROP CONSTRAINT "FK_cd4eb233bf2c800ec2621945efe"`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" ALTER COLUMN "genre_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "project_genres" DROP COLUMN "subgenre_id"`, + ); + await queryRunner.query(`ALTER TABLE "genres" DROP COLUMN "type"`); + await queryRunner.query( + `ALTER TABLE "project_genres" ADD CONSTRAINT "FK_44a67d9910f540fff30cfb44529" FOREIGN KEY ("genre_id") REFERENCES "genres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1730338365708-add-episodic-genres.ts b/backend/src/database/migrations/1730338365708-add-episodic-genres.ts new file mode 100644 index 0000000..06cdd6d --- /dev/null +++ b/backend/src/database/migrations/1730338365708-add-episodic-genres.ts @@ -0,0 +1,97 @@ +import { Genre } from 'src/genres/entities/genre.entity'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddEpisodicGenres1730338365708 implements MigrationInterface { + episodicGenres = [ + { + id: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + type: 'episodic', + name: 'Drama', + }, + { + id: '3170f9cc-338a-4478-a14c-39bce63870d0', + type: 'episodic', + name: 'Comedy', + }, + { + id: '6f1785ef-b59b-4032-acc1-705f0aece2e6', + type: 'episodic', + name: 'Action & Adventure', + }, + { + id: '932a7b0e-b07b-4829-9e17-36b13805c516', + type: 'episodic', + name: 'Science Fiction & Fantasy', + }, + { + id: '88ea9593-12c0-4308-8157-c8ed5cf85568', + type: 'episodic', + name: 'Mystery & Thriller', + }, + { + id: '264f275e-87ec-4f91-9c64-28264d869375', + type: 'episodic', + name: 'Horror', + }, + { + id: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + type: 'episodic', + name: 'Reality TV', + }, + { + id: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + type: 'episodic', + name: 'Documentary', + }, + { + id: '57751942-f0ba-499a-b220-7985059bc194', + type: 'episodic', + name: 'Animated Series', + }, + { + id: 'e90710dd-cac4-4997-923a-78b19d778876', + type: 'episodic', + name: "Children's Programming", + }, + { + id: '62c3e54a-cf91-4a01-9eb3-f6e5ba70b74e', + type: 'episodic', + name: 'Variety & Talk Shows', + }, + { + id: '03e341d5-0338-406b-9672-64a06cf2b831', + type: 'episodic', + name: 'News & Current Affairs', + }, + { + id: '094777d5-5926-43bf-a384-60f800fb6010', + type: 'episodic', + name: 'Game Shows', + }, + { + id: '7e32a2d4-9615-49c0-9238-d10f997c43d5', + type: 'episodic', + name: 'Sports', + }, + { + id: 'b492be4d-0011-443a-9094-9b927e2650a5', + type: 'episodic', + name: 'Music Television', + }, + ]; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager.save( + queryRunner.manager.create>( + Genre, + this.episodicGenres, + ), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.delete(Genre, { + type: 'episodic', + }); + } +} diff --git a/backend/src/database/migrations/1730339388162-migrate-project-genres.ts b/backend/src/database/migrations/1730339388162-migrate-project-genres.ts new file mode 100644 index 0000000..ef4e907 --- /dev/null +++ b/backend/src/database/migrations/1730339388162-migrate-project-genres.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MigrateProjectGenres1730339388162 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // set project_genres subgenre_id to the first subgenre of the genre_id + await queryRunner.query(` + UPDATE project_genres + SET subgenre_id = sub.id + FROM ( + SELECT s.id, s.genre_id + FROM subgenres s + JOIN genres g ON s.genre_id = g.id + ORDER BY s.created_at + ) sub + WHERE project_genres.genre_id = sub.genre_id + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // set project_genres subgenre_id to null + await queryRunner.query(` + UPDATE project_genres + SET subgenre_id = NULL + `); + } +} diff --git a/backend/src/database/migrations/1730340005682-remove-last-genres-rename-project-genres.ts b/backend/src/database/migrations/1730340005682-remove-last-genres-rename-project-genres.ts new file mode 100644 index 0000000..c40f318 --- /dev/null +++ b/backend/src/database/migrations/1730340005682-remove-last-genres-rename-project-genres.ts @@ -0,0 +1,30 @@ +import { In, MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveLastGenresRenameProjectGenres1730340005682 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const genresToRemove = [ + 'bfb5f952-8dd9-45f4-bb27-530fa728250c', + '8d7262a8-a94b-4f85-886a-a5ecff8844b3', + ]; + + await queryRunner.manager.delete('project_genres', { + genre_id: In(genresToRemove), + }); + + await queryRunner.manager.delete('genres', { + id: In(genresToRemove), + }); + + await queryRunner.query( + `ALTER TABLE "project_genres" RENAME TO "project_subgenres"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "project_subgenres" RENAME TO "project_genres"`, + ); + } +} diff --git a/backend/src/database/migrations/1730377610292-remove-episodic-projectGenres.ts b/backend/src/database/migrations/1730377610292-remove-episodic-projectGenres.ts new file mode 100644 index 0000000..67fdcb8 --- /dev/null +++ b/backend/src/database/migrations/1730377610292-remove-episodic-projectGenres.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveEpisodicProjectGenres1730377610292 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // delete all projectGenres with projectGenres.project.type = 'episodic' + queryRunner.manager.query(` + DELETE FROM project_subgenres + WHERE "project_id" IN ( + SELECT id + FROM projects + WHERE type = 'episodic' + ) + `); + } + + public async down(): Promise { + // no need to rollback this migration + } +} diff --git a/backend/src/database/migrations/1730990068405-seed-episodic-subgenres.ts b/backend/src/database/migrations/1730990068405-seed-episodic-subgenres.ts new file mode 100644 index 0000000..f0b4799 --- /dev/null +++ b/backend/src/database/migrations/1730990068405-seed-episodic-subgenres.ts @@ -0,0 +1,407 @@ +import { randomUUID } from 'node:crypto'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeedEpisodicSubgenres1730990068405 implements MigrationInterface { + subgenres = [ + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Medical Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Legal Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Crime Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Police Procedural', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Courtroom Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Political Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Historical Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Period Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Teen Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Family Drama', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Soap Opera', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Telenovela', + }, + { + id: randomUUID(), + genreId: 'f49d5884-842b-4f8e-b64f-c3f9a4e335d2', + name: 'Anthology Drama', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Sitcom (Situational Comedy)', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Romantic Comedy', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Workplace Comedy', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Family Sitcom', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Dark Comedy', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Sketch Comedy', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Stand-up Comedy', + }, + { + id: randomUUID(), + genreId: '3170f9cc-338a-4478-a14c-39bce63870d0', + name: 'Animated Comedy', + }, + { + id: randomUUID(), + genreId: '6f1785ef-b59b-4032-acc1-705f0aece2e6', + name: 'Spy/Espionage', + }, + { + id: randomUUID(), + genreId: '6f1785ef-b59b-4032-acc1-705f0aece2e6', + name: 'Superhero', + }, + { + id: randomUUID(), + genreId: '6f1785ef-b59b-4032-acc1-705f0aece2e6', + name: 'Martial Arts', + }, + { + id: randomUUID(), + genreId: '6f1785ef-b59b-4032-acc1-705f0aece2e6', + name: 'Military Action', + }, + { + id: randomUUID(), + genreId: '932a7b0e-b07b-4829-9e17-36b13805c516', + name: 'Space Opera', + }, + { + id: randomUUID(), + genreId: '932a7b0e-b07b-4829-9e17-36b13805c516', + name: 'Time Travel', + }, + { + id: randomUUID(), + genreId: '932a7b0e-b07b-4829-9e17-36b13805c516', + name: 'Dystopian/Post-Apocalyptic', + }, + { + id: randomUUID(), + genreId: '932a7b0e-b07b-4829-9e17-36b13805c516', + name: 'Supernatural', + }, + { + id: randomUUID(), + genreId: '932a7b0e-b07b-4829-9e17-36b13805c516', + name: 'Urban Fantasy', + }, + { + id: randomUUID(), + genreId: '932a7b0e-b07b-4829-9e17-36b13805c516', + name: 'High Fantasy', + }, + { + id: randomUUID(), + genreId: '88ea9593-12c0-4308-8157-c8ed5cf85568', + name: 'Detective Series', + }, + { + id: randomUUID(), + genreId: '88ea9593-12c0-4308-8157-c8ed5cf85568', + name: 'Psychological Thriller', + }, + { + id: randomUUID(), + genreId: '88ea9593-12c0-4308-8157-c8ed5cf85568', + name: 'Crime Thriller', + }, + { + id: randomUUID(), + genreId: '88ea9593-12c0-4308-8157-c8ed5cf85568', + name: 'Supernatural Mystery', + }, + { + id: randomUUID(), + genreId: '264f275e-87ec-4f91-9c64-28264d869375', + name: 'Supernatural Horror', + }, + { + id: randomUUID(), + genreId: '264f275e-87ec-4f91-9c64-28264d869375', + name: 'Slasher', + }, + { + id: randomUUID(), + genreId: '264f275e-87ec-4f91-9c64-28264d869375', + name: 'Psychological Horror', + }, + { + id: randomUUID(), + genreId: '264f275e-87ec-4f91-9c64-28264d869375', + name: 'Zombie', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Competition Shows', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Talent Shows', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Dating Shows', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Lifestyle', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Home Improvement', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Cooking Shows', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Travel Shows', + }, + { + id: randomUUID(), + genreId: 'a0bf144a-9bb7-4152-b30f-6d52ad064cf4', + name: 'Survival Shows', + }, + { + id: randomUUID(), + genreId: '57751942-f0ba-499a-b220-7985059bc194', + name: 'Adult Animation', + }, + { + id: randomUUID(), + genreId: '57751942-f0ba-499a-b220-7985059bc194', + name: "Children's Animation", + }, + { + id: randomUUID(), + genreId: '57751942-f0ba-499a-b220-7985059bc194', + name: 'Anime', + }, + { + id: randomUUID(), + genreId: 'e90710dd-cac4-4997-923a-78b19d778876', + name: 'Educational', + }, + { + id: randomUUID(), + genreId: 'e90710dd-cac4-4997-923a-78b19d778876', + name: 'Adventure', + }, + { + id: randomUUID(), + genreId: 'e90710dd-cac4-4997-923a-78b19d778876', + name: 'Fantasy', + }, + { + id: randomUUID(), + genreId: '62c3e54a-cf91-4a01-9eb3-f6e5ba70b74e', + name: 'Late Night Talk Shows', + }, + { + id: randomUUID(), + genreId: '62c3e54a-cf91-4a01-9eb3-f6e5ba70b74e', + name: 'Daytime Talk Shows', + }, + { + id: randomUUID(), + genreId: '62c3e54a-cf91-4a01-9eb3-f6e5ba70b74e', + name: 'Sketch Comedy Shows', + }, + { + id: randomUUID(), + genreId: '03e341d5-0338-406b-9672-64a06cf2b831', + name: 'News Broadcasts', + }, + { + id: randomUUID(), + genreId: '03e341d5-0338-406b-9672-64a06cf2b831', + name: 'Political Commentary', + }, + { + id: randomUUID(), + genreId: '03e341d5-0338-406b-9672-64a06cf2b831', + name: 'Investigative Journalism', + }, + { + id: randomUUID(), + genreId: '094777d5-5926-43bf-a384-60f800fb6010', + name: 'Quiz Shows', + }, + { + id: randomUUID(), + genreId: '094777d5-5926-43bf-a384-60f800fb6010', + name: 'Panel Shows', + }, + { + id: randomUUID(), + genreId: '7e32a2d4-9615-49c0-9238-d10f997c43d5', + name: 'Sports Commentary', + }, + { + id: randomUUID(), + genreId: 'b492be4d-0011-443a-9094-9b927e2650a5', + name: 'Music Videos', + }, + { + id: randomUUID(), + genreId: 'b492be4d-0011-443a-9094-9b927e2650a5', + name: 'Concert Broadcasts', + }, + { + id: randomUUID(), + genreId: 'b492be4d-0011-443a-9094-9b927e2650a5', + name: 'Music Competition Shows', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'True Crime', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Science & Technology', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Travel', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Observational', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Expository', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Performative', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Poetic', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Reflexive', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Historical', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Biographical', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Nature/Wildlife', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Social Issue', + }, + { + id: randomUUID(), + genreId: '5b6ad647-d49b-4d4b-84c9-8b84bb5ebb86', + name: 'Docudrama', + }, + ]; + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager.save('subgenres', this.subgenres); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.query(` + DELETE FROM subgenres + WHERE id IN (${this.subgenres.map((subgenre) => `'${subgenre.id}'`).join(',')}) + `); + } +} diff --git a/backend/src/database/migrations/1731503600876-added-deleted-at-users.ts b/backend/src/database/migrations/1731503600876-added-deleted-at-users.ts new file mode 100644 index 0000000..72a6054 --- /dev/null +++ b/backend/src/database/migrations/1731503600876-added-deleted-at-users.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedDeletedAtUsers1731503600876 implements MigrationInterface { + name = 'AddedDeletedAtUsers1731503600876'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "project_subgenres" DROP CONSTRAINT "FK_cd4eb233bf2c800ec2621945efe"`, + ); + await queryRunner.query( + `ALTER TABLE "project_subgenres" DROP CONSTRAINT "FK_e4484a8f40c2c996bb65e10f556"`, + ); + await queryRunner.query( + `ALTER TABLE "users" ADD "deleted_at" TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `ALTER TABLE "project_subgenres" ADD CONSTRAINT "FK_3160427400524502fa3c143957e" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project_subgenres" ADD CONSTRAINT "FK_faaab998c2d8101c468d99156ec" FOREIGN KEY ("subgenre_id") REFERENCES "subgenres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "project_subgenres" DROP CONSTRAINT "FK_faaab998c2d8101c468d99156ec"`, + ); + await queryRunner.query( + `ALTER TABLE "project_subgenres" DROP CONSTRAINT "FK_3160427400524502fa3c143957e"`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "deleted_at"`); + await queryRunner.query( + `ALTER TABLE "project_subgenres" ADD CONSTRAINT "FK_e4484a8f40c2c996bb65e10f556" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project_subgenres" ADD CONSTRAINT "FK_cd4eb233bf2c800ec2621945efe" FOREIGN KEY ("subgenre_id") REFERENCES "subgenres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1732560824095-added-deleted-at-filmmaker.ts b/backend/src/database/migrations/1732560824095-added-deleted-at-filmmaker.ts new file mode 100644 index 0000000..4979fc1 --- /dev/null +++ b/backend/src/database/migrations/1732560824095-added-deleted-at-filmmaker.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedDeletedAtFilmmaker1732560824095 + implements MigrationInterface +{ + name = 'AddedDeletedAtFilmmaker1732560824095'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "deleted_at" TIMESTAMP WITH TIME ZONE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "deleted_at"`, + ); + } +} diff --git a/backend/src/database/migrations/1732567977813-added-genre-id-projects.ts b/backend/src/database/migrations/1732567977813-added-genre-id-projects.ts new file mode 100644 index 0000000..fd5719c --- /dev/null +++ b/backend/src/database/migrations/1732567977813-added-genre-id-projects.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedGenreIdProjects1732567977813 implements MigrationInterface { + name = 'AddedGenreIdProjects1732567977813'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "projects" ADD "genre_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "projects" ADD CONSTRAINT "FK_954399415c848564fa9c41c37ae" FOREIGN KEY ("genre_id") REFERENCES "genres"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "projects" DROP CONSTRAINT "FK_954399415c848564fa9c41c37ae"`, + ); + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "genre_id"`); + } +} diff --git a/backend/src/database/migrations/1732568041034-add-genres-projects.ts b/backend/src/database/migrations/1732568041034-add-genres-projects.ts new file mode 100644 index 0000000..64ffcc2 --- /dev/null +++ b/backend/src/database/migrations/1732568041034-add-genres-projects.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddGenresProjects1732568041034 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // set project.genre_id to the first project_subgenres genre_id of the project + await queryRunner.query(` + UPDATE projects + SET genre_id = ( + SELECT genre_id + FROM project_subgenres + WHERE project_subgenres.project_id = projects.id + ORDER BY project_subgenres.created_at + LIMIT 1 + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`UPDATE projects SET genre_id = NULL`); + } +} diff --git a/backend/src/database/migrations/1733770884555-added-withdrawal-frequency.ts b/backend/src/database/migrations/1733770884555-added-withdrawal-frequency.ts new file mode 100644 index 0000000..b0d4a81 --- /dev/null +++ b/backend/src/database/migrations/1733770884555-added-withdrawal-frequency.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedWithdrawalFrequency1733770884555 + implements MigrationInterface +{ + name = 'AddedWithdrawalFrequency1733770884555'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "filmmakers" DROP COLUMN "lightning_address"`, + ); + await queryRunner.query( + `ALTER TABLE "payment_methods" ADD "withdrawal_frequency" character varying NOT NULL DEFAULT 'automatic'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payment_methods" DROP COLUMN "withdrawal_frequency"`, + ); + await queryRunner.query( + `ALTER TABLE "filmmakers" ADD "lightning_address" character varying`, + ); + } +} diff --git a/backend/src/database/migrations/1736516158098-added-cognito-id.ts b/backend/src/database/migrations/1736516158098-added-cognito-id.ts new file mode 100644 index 0000000..4538948 --- /dev/null +++ b/backend/src/database/migrations/1736516158098-added-cognito-id.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedCognitoId1736516158098 implements MigrationInterface { + name = 'AddedCognitoId1736516158098'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" ADD "cognito_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "users" ADD CONSTRAINT "UQ_d9dea74916617da4a95c8cce52a" UNIQUE ("cognito_id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" DROP CONSTRAINT "UQ_d9dea74916617da4a95c8cce52a"`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "cognito_id"`); + } +} diff --git a/backend/src/database/migrations/1738500000000-add-dual-content-delivery.ts b/backend/src/database/migrations/1738500000000-add-dual-content-delivery.ts new file mode 100644 index 0000000..e001c3a --- /dev/null +++ b/backend/src/database/migrations/1738500000000-add-dual-content-delivery.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Adds dual-mode content delivery columns to the projects table. + * - deliveryMode: 'native' (MinIO/FFmpeg) or 'partner' (external CDN) + * - Partner-specific streaming URLs + * - Native streaming URL (HLS manifest path) + * Also creates the content_keys table for AES-128 encryption. + */ +export class AddDualContentDelivery1738500000000 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // Add columns to projects table + await queryRunner.query(` + ALTER TABLE "projects" + ADD COLUMN IF NOT EXISTS "delivery_mode" varchar DEFAULT 'native', + ADD COLUMN IF NOT EXISTS "partner_stream_url" varchar, + ADD COLUMN IF NOT EXISTS "partner_dash_url" varchar, + ADD COLUMN IF NOT EXISTS "partner_fairplay_url" varchar, + ADD COLUMN IF NOT EXISTS "partner_drm_token" varchar, + ADD COLUMN IF NOT EXISTS "partner_api_base_url" varchar, + ADD COLUMN IF NOT EXISTS "streaming_url" varchar + `); + + // Create content_keys table for AES-128 encryption + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "content_keys" ( + "id" varchar PRIMARY KEY, + "content_id" varchar NOT NULL, + "key_data" bytea NOT NULL, + "iv" bytea, + "rotation_index" int DEFAULT 0, + "created_at" timestamptz DEFAULT NOW() + ) + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_content_keys_content_id" + ON "content_keys" ("content_id") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "content_keys"`); + await queryRunner.query(` + ALTER TABLE "projects" + DROP COLUMN IF EXISTS "delivery_mode", + DROP COLUMN IF EXISTS "partner_stream_url", + DROP COLUMN IF EXISTS "partner_dash_url", + DROP COLUMN IF EXISTS "partner_fairplay_url", + DROP COLUMN IF EXISTS "partner_drm_token", + DROP COLUMN IF EXISTS "partner_api_base_url", + DROP COLUMN IF EXISTS "streaming_url" + `); + } +} diff --git a/backend/src/database/migrations/1744225980141-add-drm-attributes.ts b/backend/src/database/migrations/1744225980141-add-drm-attributes.ts new file mode 100644 index 0000000..decc14e --- /dev/null +++ b/backend/src/database/migrations/1744225980141-add-drm-attributes.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDrmAttributes1744225980141 implements MigrationInterface { + name = 'AddDrmAttributes1744225980141'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" ADD "drm_content_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "contents" ADD "drm_media_id" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" DROP COLUMN "drm_media_id"`, + ); + await queryRunner.query( + `ALTER TABLE "contents" DROP COLUMN "drm_content_id"`, + ); + } +} diff --git a/backend/src/database/migrations/1746041212176-add-payout.ts b/backend/src/database/migrations/1746041212176-add-payout.ts new file mode 100644 index 0000000..3a216d4 --- /dev/null +++ b/backend/src/database/migrations/1746041212176-add-payout.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPayout1746041212176 implements MigrationInterface { + name = 'AddPayout1746041212176'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "payouts" ("id" character varying NOT NULL, "amount" numeric(20,10) NOT NULL DEFAULT '0', "user_id" character varying NOT NULL, "period_start" TIMESTAMP WITH TIME ZONE NOT NULL, "period_end" TIMESTAMP WITH TIME ZONE NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_76855dc4f0a6c18c72eea302e87" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "payouts" ADD CONSTRAINT "FK_5d4cccb2284ac44a2781a832786" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payouts" DROP CONSTRAINT "FK_5d4cccb2284ac44a2781a832786"`, + ); + + await queryRunner.query(`DROP TABLE "payouts"`); + } +} diff --git a/backend/src/database/migrations/1750956597224-add-flash-subscription-id.ts b/backend/src/database/migrations/1750956597224-add-flash-subscription-id.ts new file mode 100644 index 0000000..6da8629 --- /dev/null +++ b/backend/src/database/migrations/1750956597224-add-flash-subscription-id.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddFlashSubscriptionId1750956597224 implements MigrationInterface { + name = 'AddFlashSubscriptionId1750956597224'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" ADD "flash_id" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" DROP COLUMN "flash_id"`, + ); + } +} diff --git a/backend/src/database/migrations/1751655104496-add-subscription-reminder-date.ts b/backend/src/database/migrations/1751655104496-add-subscription-reminder-date.ts new file mode 100644 index 0000000..1cf46e7 --- /dev/null +++ b/backend/src/database/migrations/1751655104496-add-subscription-reminder-date.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSubscriptionReminderDate1751655104496 + implements MigrationInterface +{ + name = 'AddSubscriptionReminderDate1751655104496'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" ADD "expiration_reminder_sent_at" TIMESTAMP WITH TIME ZONE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "subscriptions" DROP COLUMN "expiration_reminder_sent_at"`, + ); + } +} diff --git a/backend/src/database/migrations/1755016652731-add-seasons.ts b/backend/src/database/migrations/1755016652731-add-seasons.ts new file mode 100644 index 0000000..5b9ce48 --- /dev/null +++ b/backend/src/database/migrations/1755016652731-add-seasons.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSeasons1755016652731 implements MigrationInterface { + name = 'AddSeasons1755016652731'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "season_rents" ("id" character varying NOT NULL, "season_id" character varying NOT NULL, "user_id" character varying NOT NULL, "usd_amount" numeric(5,2) NOT NULL, "status" character varying NOT NULL, "provider_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_f85d4fc16f10cb40b14c4a4cddc" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_c7b3ae878ea8c280c6f458c918" ON "season_rents" ("season_id", "user_id") `, + ); + await queryRunner.query( + `CREATE TABLE "seasons" ("id" character varying NOT NULL, "project_id" character varying NOT NULL, "season_number" integer NOT NULL, "rental_price" numeric(10,2) NOT NULL DEFAULT '0', "title" character varying, "description" character varying, "is_active" boolean NOT NULL DEFAULT true, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_cb8ed53b5fe109dcd4a4449ec9d" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_f4a5a38f2a47d557411a33be5c" ON "seasons" ("project_id", "season_number") `, + ); + await queryRunner.query( + `ALTER TABLE "season_rents" ADD CONSTRAINT "FK_a5a5048cf1bb48b543ead204ffb" FOREIGN KEY ("season_id") REFERENCES "seasons"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "season_rents" ADD CONSTRAINT "FK_b34e3b0a52120963f32ef44629d" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "seasons" ADD CONSTRAINT "FK_fd06ddabd0ad6e52dfdec686324" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "seasons" DROP CONSTRAINT "FK_fd06ddabd0ad6e52dfdec686324"`, + ); + await queryRunner.query( + `ALTER TABLE "season_rents" DROP CONSTRAINT "FK_b34e3b0a52120963f32ef44629d"`, + ); + await queryRunner.query( + `ALTER TABLE "season_rents" DROP CONSTRAINT "FK_a5a5048cf1bb48b543ead204ffb"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_f4a5a38f2a47d557411a33be5c"`, + ); + await queryRunner.query(`DROP TABLE "seasons"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_c7b3ae878ea8c280c6f458c918"`, + ); + await queryRunner.query(`DROP TABLE "season_rents"`); + } +} diff --git a/backend/src/database/migrations/1755279221113-populate-season.ts b/backend/src/database/migrations/1755279221113-populate-season.ts new file mode 100644 index 0000000..87b21d2 --- /dev/null +++ b/backend/src/database/migrations/1755279221113-populate-season.ts @@ -0,0 +1,247 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PopulateSeason1755279221113 implements MigrationInterface { + name = 'PopulateSeason1755279221113'; + public async up(queryRunner: QueryRunner): Promise { + console.log('🚀 Starting seasons population migration...'); + + try { + // Get migration statistics + const statsQuery = ` + SELECT + COUNT(DISTINCT p.id) as total_episodic_projects, + COUNT(DISTINCT CONCAT(p.id, '_', c.season)) as unique_seasons, + COUNT(c.id) as total_episodes + FROM projects p + INNER JOIN contents c ON c.project_id = p.id + WHERE p.type = 'episodic' + AND c.season IS NOT NULL + AND p.deleted_at IS NULL + AND c.deleted_at IS NULL + `; + + const [stats] = await queryRunner.query(statsQuery); + console.log(`📊 Migration Stats: + - Episodic Projects: ${stats.total_episodic_projects} + - Unique Seasons: ${stats.unique_seasons} + - Total Episodes: ${stats.total_episodes}`); + + if (stats.unique_seasons === '0') { + console.log( + '⚠️ No episodic content with season information found. Skipping migration.', + ); + return; + } + + // Query to get grouped season data + const seasonsDataQuery = ` + SELECT + p.id as project_id, + p.name as project_name, + p.rental_price as project_rental_price, + c.season as season_number, + COALESCE(AVG(NULLIF(c.rental_price, 0)), p.rental_price, 0) as avg_rental_price, + COUNT(c.id) as episode_count, + MIN(c.created_at) as earliest_episode_date + FROM projects p + INNER JOIN contents c ON c.project_id = p.id + WHERE p.type = 'episodic' + AND c.season IS NOT NULL + AND p.deleted_at IS NULL + AND c.deleted_at IS NULL + GROUP BY p.id, p.name, p.rental_price, c.season + ORDER BY p.name ASC, c.season ASC + `; + + const seasonsData = await queryRunner.query(seasonsDataQuery); + let createdSeasonsCount = 0; + let skippedSeasonsCount = 0; + + // Process each season + for (const seasonData of seasonsData) { + // Check if the season already exists + const existingSeasonQuery = ` + SELECT id FROM seasons + WHERE project_id = $1 AND season_number = $2 + `; + + const [existingSeason] = await queryRunner.query(existingSeasonQuery, [ + seasonData.project_id, + seasonData.season_number, + ]); + + if (existingSeason) { + console.log( + `⚠️ Season ${seasonData.season_number} already exists for project "${seasonData.project_name}"`, + ); + skippedSeasonsCount++; + continue; + } + + // Create new season + const [{ uuid: seasonId }] = await queryRunner.query( + 'SELECT gen_random_uuid() as uuid', + ); + const title = this.generateSeasonTitle( + seasonData.project_name, + seasonData.season_number, + ); + const description = this.generateSeasonDescription( + seasonData.season_number, + seasonData.episode_count, + ); + + const insertSeasonQuery = ` + INSERT INTO seasons ( + id, project_id, season_number, rental_price, + title, description, is_active, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW()) + `; + + await queryRunner.query(insertSeasonQuery, [ + seasonId, + seasonData.project_id, + seasonData.season_number, + 0, + title, + description, + true, + seasonData.earliest_episode_date, + ]); + + console.log(`✅ Created Season ${seasonData.season_number} for "${seasonData.project_name}" + (${seasonData.episode_count} episodes, $0)`); + + createdSeasonsCount++; + } + + // Check data integrity + await this.verifyDataIntegrity(queryRunner); + + console.log(`🎉 Migration completed successfully! + - Seasons Created: ${createdSeasonsCount} + - Seasons Skipped: ${skippedSeasonsCount} + - Total Processed: ${seasonsData.length}`); + } catch (error) { + console.error('❌ Migration failed:', error); + throw error; + } + } + + public async down(queryRunner: QueryRunner): Promise { + console.log('🔄 Rolling back seasons population...'); + + try { + // Count seasons to delete + const countQuery = ` + SELECT COUNT(*) as count + FROM seasons s + INNER JOIN projects p ON p.id = s.project_id + WHERE p.type = 'episodic' + `; + + const [{ count }] = await queryRunner.query(countQuery); + + if (count === '0') { + console.log('ℹ️ No seasons to rollback.'); + return; + } + + // Delete related season_rents first (if any) + const deleteRentsQuery = ` + DELETE FROM season_rents + WHERE season_id IN ( + SELECT s.id FROM seasons s + INNER JOIN projects p ON p.id = s.project_id + WHERE p.type = 'episodic' + ) + `; + + const rentsResult = await queryRunner.query(deleteRentsQuery); + console.log(`🗑️ Deleted ${rentsResult.affectedRows || 0} season rents`); + + // Delete temporary seasons + const deleteSeasonsQuery = ` + DELETE FROM seasons + WHERE project_id IN ( + SELECT id FROM projects WHERE type = 'episodic' + ) + `; + + const seasonsResult = await queryRunner.query(deleteSeasonsQuery); + console.log( + `✅ Rollback completed: Deleted ${seasonsResult.affectedRows || count} seasons`, + ); + } catch (error) { + console.error('❌ Rollback failed:', error); + throw error; + } + } + + /** + * Generate title for the season + */ + private generateSeasonTitle( + projectName: string, + seasonNumber: number, + ): string { + return `${projectName} - Season ${seasonNumber}`; + } + + /** + * Generates a description based on the season number and episode count + */ + private generateSeasonDescription( + seasonNumber: number, + episodeCount: number, + ): string { + const episodeText = episodeCount === 1 ? 'episode' : 'episodes'; + return `Season ${seasonNumber} contains ${episodeCount} ${episodeText}`; + } + + /** + * Verify data integrity after migration + */ + private async verifyDataIntegrity(queryRunner: QueryRunner): Promise { + console.log('🔍 Verifying data integrity...'); + + // Verify that there are no duplicates + const duplicatesQuery = ` + SELECT project_id, season_number, COUNT(*) as count + FROM seasons + GROUP BY project_id, season_number + HAVING COUNT(*) > 1 + `; + + const duplicates = await queryRunner.query(duplicatesQuery); + if (duplicates.length > 0) { + console.warn('⚠️ Found duplicate seasons:', duplicates); + } + + // Verify that all seasons have episodes + const orphanSeasonsQuery = ` + SELECT s.id, s.project_id, s.season_number + FROM seasons s + LEFT JOIN contents c ON c.project_id = s.project_id AND c.season = s.season_number + WHERE c.id IS NULL + `; + + const orphanSeasons = await queryRunner.query(orphanSeasonsQuery); + if (orphanSeasons.length > 0) { + console.warn('⚠️ Found seasons without episodes:', orphanSeasons); + } + + // Final statistics + const finalStatsQuery = ` + SELECT COUNT(*) as total_seasons + FROM seasons s + INNER JOIN projects p ON p.id = s.project_id + WHERE p.type = 'episodic' + `; + + const [{ total_seasons }] = await queryRunner.query(finalStatsQuery); + console.log( + `✅ Data integrity check completed. Total seasons in DB: ${total_seasons}`, + ); + } +} diff --git a/backend/src/database/migrations/1755538160397-add-series-relation-to-content.ts b/backend/src/database/migrations/1755538160397-add-series-relation-to-content.ts new file mode 100644 index 0000000..6d53dbf --- /dev/null +++ b/backend/src/database/migrations/1755538160397-add-series-relation-to-content.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSeriesRelationToContent1755538160397 + implements MigrationInterface +{ + name = 'AddSeriesRelationToContent1755538160397'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" ADD "season_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "contents" ADD CONSTRAINT "FK_201d6e4b373007c3119f4ce39cf" FOREIGN KEY ("season_id") REFERENCES "seasons"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" DROP CONSTRAINT "FK_201d6e4b373007c3119f4ce39cf"`, + ); + await queryRunner.query(`ALTER TABLE "contents" DROP COLUMN "season_id"`); + } +} diff --git a/backend/src/database/migrations/1755538639470-link-contents-to-season.ts b/backend/src/database/migrations/1755538639470-link-contents-to-season.ts new file mode 100644 index 0000000..b81de99 --- /dev/null +++ b/backend/src/database/migrations/1755538639470-link-contents-to-season.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class LinkContentsToSeason1755538639470 implements MigrationInterface { + name = 'LinkContentsToSeason1755538639470'; + + public async up(queryRunner: QueryRunner): Promise { + console.log('🔄 Starting content-season linking migration...'); + + try { + const result = await queryRunner.query(` + UPDATE contents + SET season_id = seasons.id + FROM seasons + WHERE contents.project_id = seasons.project_id + AND contents.season = seasons.season_number + AND contents.season_id IS NULL + AND contents.season IS NOT NULL + `); + + console.log( + `✅ Successfully linked ${result[1] || 0} contents to their seasons`, + ); + } catch (error) { + console.error('❌ Error during migration:', error); + throw error; + } + } + + public async down(queryRunner: QueryRunner): Promise { + console.log('🔄 Reverting content-season linking migration...'); + + try { + const result = await queryRunner.query(` + UPDATE contents + SET season_id = NULL + WHERE season_id IN ( + SELECT seasons.id + FROM seasons + WHERE contents.project_id = seasons.project_id + AND contents.season = seasons.season_number + ) + `); + + console.log( + `✅ Successfully unlinked ${result[1] || 0} contents from their seasons`, + ); + } catch (error) { + console.error('❌ Error during rollback:', error); + throw error; + } + } +} diff --git a/backend/src/database/migrations/1756217864126-drop-pk-constraint-to-content-id-at-shareholder.ts b/backend/src/database/migrations/1756217864126-drop-pk-constraint-to-content-id-at-shareholder.ts new file mode 100644 index 0000000..da1a8f1 --- /dev/null +++ b/backend/src/database/migrations/1756217864126-drop-pk-constraint-to-content-id-at-shareholder.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DropPkConstraintToContentIdAtShareholder1756217864126 + implements MigrationInterface +{ + name = 'DropPkConstraintToContentIdAtShareholder1756217864126'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("id", "filmmaker_id")`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "id")`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "content_id", "id")`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "content_id", "id")`, + ); + } +} diff --git a/backend/src/database/migrations/1756217955871-set-content-id-as-nullable-at-shareholders.ts b/backend/src/database/migrations/1756217955871-set-content-id-as-nullable-at-shareholders.ts new file mode 100644 index 0000000..1db331e --- /dev/null +++ b/backend/src/database/migrations/1756217955871-set-content-id-as-nullable-at-shareholders.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SetContentIdAsNullableAtShareholders1756217955871 + implements MigrationInterface +{ + name = 'SetContentIdAsNullableAtShareholders1756217955871'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "content_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "content_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1756218047182-add-season-id-to-shareholder.ts b/backend/src/database/migrations/1756218047182-add-season-id-to-shareholder.ts new file mode 100644 index 0000000..57b28cd --- /dev/null +++ b/backend/src/database/migrations/1756218047182-add-season-id-to-shareholder.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSeasonIdToShareholder1756218047182 + implements MigrationInterface +{ + name = 'AddSeasonIdToShareholder1756218047182'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ADD "season_id" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP COLUMN "season_id"`, + ); + } +} diff --git a/backend/src/database/migrations/1756224258531-revert-add-season-id-to-shareholder.ts b/backend/src/database/migrations/1756224258531-revert-add-season-id-to-shareholder.ts new file mode 100644 index 0000000..65d7d93 --- /dev/null +++ b/backend/src/database/migrations/1756224258531-revert-add-season-id-to-shareholder.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RevertAddSeasonIdToShareholder1756224258531 + implements MigrationInterface +{ + name = 'RevertAddSeasonIdToShareholder1756224258531'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP COLUMN "season_id"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" ADD "season_id" character varying`, + ); + } +} diff --git a/backend/src/database/migrations/1756224398406-revert-set-content-id-as-nullable-at-shareholders.ts b/backend/src/database/migrations/1756224398406-revert-set-content-id-as-nullable-at-shareholders.ts new file mode 100644 index 0000000..5cad875 --- /dev/null +++ b/backend/src/database/migrations/1756224398406-revert-set-content-id-as-nullable-at-shareholders.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RevertSetContentIdAsNullableAtShareholders1756224398406 + implements MigrationInterface +{ + name = 'RevertSetContentIdAsNullableAtShareholders1756224398406'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "content_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "content_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/database/migrations/1756224574368-revert-drop-pk-constraint-to-content-id-at-shareholder.ts b/backend/src/database/migrations/1756224574368-revert-drop-pk-constraint-to-content-id-at-shareholder.ts new file mode 100644 index 0000000..3401d9c --- /dev/null +++ b/backend/src/database/migrations/1756224574368-revert-drop-pk-constraint-to-content-id-at-shareholder.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RevertDropPkConstraintToContentIdAtShareholder1756224574368 + implements MigrationInterface +{ + name = 'RevertDropPkConstraintToContentIdAtShareholder1756224574368'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("id", "filmmaker_id", "content_id")`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "content_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ALTER COLUMN "content_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "FK_65c4c2baa1ea057b6820a1178c9" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" DROP CONSTRAINT "PK_94958880e5b2064096130825427"`, + ); + await queryRunner.query( + `ALTER TABLE "shareholders" ADD CONSTRAINT "PK_94958880e5b2064096130825427" PRIMARY KEY ("filmmaker_id", "id")`, + ); + } +} diff --git a/backend/src/database/migrations/1756845165451-add-discount-entities.ts b/backend/src/database/migrations/1756845165451-add-discount-entities.ts new file mode 100644 index 0000000..c342fb7 --- /dev/null +++ b/backend/src/database/migrations/1756845165451-add-discount-entities.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDiscountEntities1756845165451 implements MigrationInterface { + name = 'AddDiscountEntities1756845165451'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "discounts" ("id" character varying NOT NULL, "content_id" character varying, "season_id" character varying, "coupon_code" character varying(50) NOT NULL, "discount_value" numeric(5,2) NOT NULL, "discount_type" character varying(20) NOT NULL DEFAULT 'percentage', "start_date" TIMESTAMP, "expiration_date" TIMESTAMP, "max_uses" integer, "email" character varying(255), "created_by_id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "UQ_5ce6a203b5160a73a06c75360c4" UNIQUE ("coupon_code"), CONSTRAINT "PK_66c522004212dc814d6e2f14ecc" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "discount_redemptions" ("id" character varying NOT NULL, "redeemed_at" TIMESTAMP NOT NULL DEFAULT now(), "discount_id" character varying, "user_id" character varying, CONSTRAINT "UQ_14645195785a5f65180adef0731" UNIQUE ("discount_id", "user_id"), CONSTRAINT "PK_2efe685154f7c7deacaf6f0ef63" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" ADD CONSTRAINT "FK_a9f828d89289bf23919dbb4b45f" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" ADD CONSTRAINT "FK_ac0d6fbe06296e0f2083fb31ef5" FOREIGN KEY ("season_id") REFERENCES "seasons"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" ADD CONSTRAINT "FK_6d5d8d759f5047e436b025c3478" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "FK_465af58d6d238d3e0b9af881b6e" FOREIGN KEY ("discount_id") REFERENCES "discounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "FK_e0c1fc9d5b7d3ff68b09d8e06e3" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "FK_e0c1fc9d5b7d3ff68b09d8e06e3"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "FK_465af58d6d238d3e0b9af881b6e"`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" DROP CONSTRAINT "FK_6d5d8d759f5047e436b025c3478"`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" DROP CONSTRAINT "FK_ac0d6fbe06296e0f2083fb31ef5"`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" DROP CONSTRAINT "FK_a9f828d89289bf23919dbb4b45f"`, + ); + await queryRunner.query(`DROP TABLE "discount_redemptions"`); + await queryRunner.query(`DROP TABLE "discounts"`); + } +} diff --git a/backend/src/database/migrations/1757425771687-remove-unique-constraint-from-coupon-code.ts b/backend/src/database/migrations/1757425771687-remove-unique-constraint-from-coupon-code.ts new file mode 100644 index 0000000..75b9255 --- /dev/null +++ b/backend/src/database/migrations/1757425771687-remove-unique-constraint-from-coupon-code.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveUniqueConstraintFromCouponCode1757425771687 + implements MigrationInterface +{ + name = 'RemoveUniqueConstraintFromCouponCode1757425771687'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discounts" DROP CONSTRAINT "UQ_5ce6a203b5160a73a06c75360c4"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discounts" ADD CONSTRAINT "UQ_5ce6a203b5160a73a06c75360c4" UNIQUE ("coupon_code")`, + ); + } +} diff --git a/backend/src/database/migrations/1757620018652-normalize-naming.ts b/backend/src/database/migrations/1757620018652-normalize-naming.ts new file mode 100644 index 0000000..2485b45 --- /dev/null +++ b/backend/src/database/migrations/1757620018652-normalize-naming.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class NormalizeNaming1757620018652 implements MigrationInterface { + name = 'NormalizeNaming1757620018652'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discounts" RENAME COLUMN "discount_value" TO "value"`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" RENAME COLUMN "discount_type" TO "type"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discounts" RENAME COLUMN "value" TO "discount_value"`, + ); + await queryRunner.query( + `ALTER TABLE "discounts" RENAME COLUMN "type" TO "discount_type"`, + ); + } +} diff --git a/backend/src/database/migrations/1757685339532-improve-discount-redemptions-entity.ts b/backend/src/database/migrations/1757685339532-improve-discount-redemptions-entity.ts new file mode 100644 index 0000000..cf4a77e --- /dev/null +++ b/backend/src/database/migrations/1757685339532-improve-discount-redemptions-entity.ts @@ -0,0 +1,73 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ImproveDiscountRedemptionsEntity1757685339532 + implements MigrationInterface +{ + name = 'ImproveDiscountRedemptionsEntity1757685339532'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "rental_price" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "coupon_code" character varying(50) NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "FK_465af58d6d238d3e0b9af881b6e"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "FK_e0c1fc9d5b7d3ff68b09d8e06e3"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "UQ_14645195785a5f65180adef0731"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ALTER COLUMN "discount_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ALTER COLUMN "user_id" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "UQ_14645195785a5f65180adef0731" UNIQUE ("discount_id", "user_id")`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "FK_e0c1fc9d5b7d3ff68b09d8e06e3" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "FK_465af58d6d238d3e0b9af881b6e" FOREIGN KEY ("discount_id") REFERENCES "discounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "FK_465af58d6d238d3e0b9af881b6e"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "FK_e0c1fc9d5b7d3ff68b09d8e06e3"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "UQ_14645195785a5f65180adef0731"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ALTER COLUMN "user_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ALTER COLUMN "discount_id" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "UQ_14645195785a5f65180adef0731" UNIQUE ("discount_id", "user_id")`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "FK_e0c1fc9d5b7d3ff68b09d8e06e3" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "FK_465af58d6d238d3e0b9af881b6e" FOREIGN KEY ("discount_id") REFERENCES "discounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "coupon_code"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "rental_price"`, + ); + } +} diff --git a/backend/src/database/migrations/1757687252626-add-redemption-attributes.ts b/backend/src/database/migrations/1757687252626-add-redemption-attributes.ts new file mode 100644 index 0000000..24ff332 --- /dev/null +++ b/backend/src/database/migrations/1757687252626-add-redemption-attributes.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddRedemptionAttributes1757687252626 + implements MigrationInterface +{ + name = 'AddRedemptionAttributes1757687252626'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "type" character varying(20) NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "discount_created_by_id" character varying NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "discount_created_by_id"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "type"`, + ); + } +} diff --git a/backend/src/database/migrations/1757687794422-improve-redemption-snapshot.ts b/backend/src/database/migrations/1757687794422-improve-redemption-snapshot.ts new file mode 100644 index 0000000..953c6e6 --- /dev/null +++ b/backend/src/database/migrations/1757687794422-improve-redemption-snapshot.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ImproveRedemptionSnapshot1757687794422 + implements MigrationInterface +{ + name = 'ImproveRedemptionSnapshot1757687794422'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "rental_price"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "content_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "season_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "usd_amount" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "usd_amount"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "season_id"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP COLUMN "content_id"`, + ); + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD "rental_price" numeric(10,2) NOT NULL DEFAULT '0'`, + ); + } +} diff --git a/backend/src/database/migrations/1757690122634-remove-unique-redemption-constraint.ts b/backend/src/database/migrations/1757690122634-remove-unique-redemption-constraint.ts new file mode 100644 index 0000000..0f42036 --- /dev/null +++ b/backend/src/database/migrations/1757690122634-remove-unique-redemption-constraint.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveUniqueRedemptionConstraint1757690122634 + implements MigrationInterface +{ + name = 'RemoveUniqueRedemptionConstraint1757690122634'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" DROP CONSTRAINT "UQ_14645195785a5f65180adef0731"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_redemptions" ADD CONSTRAINT "UQ_14645195785a5f65180adef0731" UNIQUE ("discount_id", "user_id")`, + ); + } +} diff --git a/backend/src/database/migrations/1758660066357-add-release-date-to-content.ts b/backend/src/database/migrations/1758660066357-add-release-date-to-content.ts new file mode 100644 index 0000000..e5287f9 --- /dev/null +++ b/backend/src/database/migrations/1758660066357-add-release-date-to-content.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddReleaseDateToContent1758660066357 + implements MigrationInterface +{ + name = 'AddReleaseDateToContent1758660066357'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" ADD "release_date" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" DROP COLUMN "release_date"`, + ); + } +} diff --git a/backend/src/database/migrations/1760403625447-added-trailer-poster-to-content.ts b/backend/src/database/migrations/1760403625447-added-trailer-poster-to-content.ts new file mode 100644 index 0000000..84019fe --- /dev/null +++ b/backend/src/database/migrations/1760403625447-added-trailer-poster-to-content.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedTrailerPosterToContent1760403625447 + implements MigrationInterface +{ + name = 'AddedTrailerPosterToContent1760403625447'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" ADD "trailer" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "contents" ADD "poster" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "contents" DROP COLUMN "poster"`); + await queryRunner.query(`ALTER TABLE "contents" DROP COLUMN "trailer"`); + } +} diff --git a/backend/src/database/migrations/1761825169884-added-trailer-transcoding.ts b/backend/src/database/migrations/1761825169884-added-trailer-transcoding.ts new file mode 100644 index 0000000..c134ea3 --- /dev/null +++ b/backend/src/database/migrations/1761825169884-added-trailer-transcoding.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedTrailerTranscoding1761825169884 + implements MigrationInterface +{ + name = 'AddedTrailerTranscoding1761825169884'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "trailers" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "file" character varying NOT NULL, "status" character varying NOT NULL DEFAULT 'processing', "metadata" json, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_598af6bec45fafbf70437f32b8b" PRIMARY KEY ("id"))`, + ); + + await queryRunner.query( + `ALTER TABLE "projects" RENAME COLUMN "trailer" TO "trailer_old"`, + ); + await queryRunner.query( + `ALTER TABLE "contents" RENAME COLUMN "trailer" TO "trailer_old"`, + ); + + await queryRunner.query(`ALTER TABLE "projects" ADD "trailer_id" uuid`); + await queryRunner.query(`ALTER TABLE "contents" ADD "trailer_id" uuid`); + await queryRunner.query( + `ALTER TABLE "projects" ADD CONSTRAINT "FK_c4208749f4307fac7b513b41e4e" FOREIGN KEY ("trailer_id") REFERENCES "trailers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "contents" ADD CONSTRAINT "FK_7dd8fb50322a5625f349b1de8d2" FOREIGN KEY ("trailer_id") REFERENCES "trailers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contents" DROP CONSTRAINT "FK_7dd8fb50322a5625f349b1de8d2"`, + ); + await queryRunner.query( + `ALTER TABLE "projects" DROP CONSTRAINT "FK_c4208749f4307fac7b513b41e4e"`, + ); + await queryRunner.query(`ALTER TABLE "contents" DROP COLUMN "trailer_id"`); + await queryRunner.query(`ALTER TABLE "projects" DROP COLUMN "trailer_id"`); + await queryRunner.query( + `ALTER TABLE "contents" RENAME COLUMN "trailer_old" TO "trailer"`, + ); + await queryRunner.query( + `ALTER TABLE "projects" RENAME COLUMN "trailer_old" TO "trailer"`, + ); + await queryRunner.query(`DROP TABLE "trailers"`); + } +} diff --git a/backend/src/database/migrations/1765398489095-added-nostr.ts b/backend/src/database/migrations/1765398489095-added-nostr.ts new file mode 100644 index 0000000..413f68f --- /dev/null +++ b/backend/src/database/migrations/1765398489095-added-nostr.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddedNostr1765398489095 implements MigrationInterface { + name = 'AddedNostr1765398489095'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" ADD "nostr_pubkey" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "users" ADD CONSTRAINT "UQ_6abb3a1e2e4fa3fcd8fb820d430" UNIQUE ("nostr_pubkey")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "users" DROP CONSTRAINT "UQ_6abb3a1e2e4fa3fcd8fb820d430"`, + ); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "nostr_pubkey"`); + } +} diff --git a/backend/src/database/migrations/1765498489095-add-library-items.ts b/backend/src/database/migrations/1765498489095-add-library-items.ts new file mode 100644 index 0000000..48ec059 --- /dev/null +++ b/backend/src/database/migrations/1765498489095-add-library-items.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddLibraryItems1765498489095 implements MigrationInterface { + name = 'AddLibraryItems1765498489095'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "library_items" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "project_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_library_user_project" UNIQUE ("user_id", "project_id"), CONSTRAINT "PK_library_items_id" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "library_items" ADD CONSTRAINT "FK_library_items_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "library_items" ADD CONSTRAINT "FK_library_items_project" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "library_items" DROP CONSTRAINT "FK_library_items_project"`, + ); + await queryRunner.query( + `ALTER TABLE "library_items" DROP CONSTRAINT "FK_library_items_user"`, + ); + await queryRunner.query(`DROP TABLE "library_items"`); + } +} diff --git a/backend/src/database/ormconfig.ts b/backend/src/database/ormconfig.ts new file mode 100644 index 0000000..3143f88 --- /dev/null +++ b/backend/src/database/ormconfig.ts @@ -0,0 +1,28 @@ +import { DataSource } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import { config } from 'dotenv'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; + +config({ path: '.env' }); +const configService = new ConfigService(); + +export default new DataSource({ + type: 'postgres', + host: configService.get('DATABASE_HOST'), + port: configService.get('DATABASE_PORT'), + username: configService.get('DATABASE_USER'), + password: configService.get('DATABASE_PASSWORD'), + database: configService.get('DATABASE_NAME'), + entities: ['dist/**/*.entity.{ts,js}'], + namingStrategy: new SnakeNamingStrategy(), + migrations: ['dist/database/migrations/*.{ts,js}'], + migrationsTableName: 'typeorm_migrations', + synchronize: false, + ssl: + configService.get('ENVIRONMENT') === 'local' || + configService.get('ENVIRONMENT') === 'development' + ? false + : { + rejectUnauthorized: false, + }, +}); diff --git a/backend/src/database/transformers/column-numeric-transformer.ts b/backend/src/database/transformers/column-numeric-transformer.ts new file mode 100644 index 0000000..35b448f --- /dev/null +++ b/backend/src/database/transformers/column-numeric-transformer.ts @@ -0,0 +1,8 @@ +export class ColumnNumericTransformer { + to(data: number): number { + return data; + } + from(data: string): number { + return Number.parseFloat(data); + } +} diff --git a/backend/src/database/validators/unique.validator.ts b/backend/src/database/validators/unique.validator.ts new file mode 100644 index 0000000..9a5742a --- /dev/null +++ b/backend/src/database/validators/unique.validator.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { EntityManager, Not } from 'typeorm'; + +@ValidatorConstraint({ name: 'Unique', async: true }) +@Injectable() +export class Unique implements ValidatorConstraintInterface { + constructor(private entityManager: EntityManager) {} + + async validate(value: string, validationArguments: ValidationArguments) { + const [EntityClass] = validationArguments.constraints; + const uniqueOrUpdate = validationArguments.object + ? { id: Not(validationArguments.object['id']) } + : {}; + + return ( + (await this.entityManager.getRepository(EntityClass).count({ + where: { + [validationArguments.property]: value, + ...uniqueOrUpdate, + }, + })) <= 0 + ); + } + + public defaultMessage(arguments_: ValidationArguments) { + const [EntityClass] = arguments_.constraints; + const entity = EntityClass || 'Entity'; + return `${entity} with the same '${arguments_.property}' already exist`; + } +} diff --git a/backend/src/discount-redemption/constants/max-free.constant.ts b/backend/src/discount-redemption/constants/max-free.constant.ts new file mode 100644 index 0000000..a2b7a44 --- /dev/null +++ b/backend/src/discount-redemption/constants/max-free.constant.ts @@ -0,0 +1 @@ +export const MAX_FREE_REDEMPTIONS_PER_FILMMAKER = 5; diff --git a/backend/src/discount-redemption/discount-redemption.module.ts b/backend/src/discount-redemption/discount-redemption.module.ts new file mode 100644 index 0000000..7766cf9 --- /dev/null +++ b/backend/src/discount-redemption/discount-redemption.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DiscountRedemptionService } from './discount-redemption.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DiscountRedemption } from './entities/discount-redemption.entity'; + +@Module({ + providers: [DiscountRedemptionService], + imports: [TypeOrmModule.forFeature([DiscountRedemption])], + exports: [DiscountRedemptionService], +}) +export class DiscountRedemptionModule {} diff --git a/backend/src/discount-redemption/discount-redemption.service.ts b/backend/src/discount-redemption/discount-redemption.service.ts new file mode 100644 index 0000000..e9a6026 --- /dev/null +++ b/backend/src/discount-redemption/discount-redemption.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { DiscountRedemption } from './entities/discount-redemption.entity'; +import { Repository } from 'typeorm'; +import { randomUUID } from 'node:crypto'; +import { + DiscountType, + DiscountTypes, +} from 'src/discounts/constants/discount-type.constant'; +import { MAX_FREE_REDEMPTIONS_PER_FILMMAKER } from './constants/max-free.constant'; +import { Discount } from 'src/discounts/entities/discount.entity'; + +export interface CreateRedemptionParameters { + userId: string; + contentId?: string; + seasonId?: string; + usdAmount: number; + couponCode: string; + discount: { + id: string; + type: DiscountType; + createdById: string; + }; +} + +@Injectable() +export class DiscountRedemptionService { + constructor( + @InjectRepository(DiscountRedemption) + private discountRedemptionRepository: Repository, + ) {} + async createRedemption({ + userId, + contentId, + seasonId, + usdAmount, + couponCode, + discount, + }: CreateRedemptionParameters) { + await this.discountRedemptionRepository.save({ + id: randomUUID(), + discountId: discount.id, + userId, + contentId, + seasonId, + type: discount.type, + discountCreatedById: discount.createdById, + usdAmount, + couponCode, + }); + } + + async canRedeem( + discountCreatedById: string, + discountType: Discount['type'], + { contentId, seasonId }: { contentId?: string; seasonId?: string }, + ): Promise { + if (discountType !== DiscountTypes.FREE) { + return true; + } + + const count = await this.discountRedemptionRepository.count({ + where: { + discountCreatedById, + type: DiscountTypes.FREE, + contentId, + seasonId, + }, + }); + return count < MAX_FREE_REDEMPTIONS_PER_FILMMAKER; + } +} diff --git a/backend/src/discount-redemption/entities/discount-redemption.entity.ts b/backend/src/discount-redemption/entities/discount-redemption.entity.ts new file mode 100644 index 0000000..9ddf514 --- /dev/null +++ b/backend/src/discount-redemption/entities/discount-redemption.entity.ts @@ -0,0 +1,72 @@ +import { DiscountType } from 'src/discounts/constants/discount-type.constant'; +import { Discount } from 'src/discounts/entities/discount.entity'; +import { User } from 'src/users/entities/user.entity'; +import { + Entity, + ManyToOne, + CreateDateColumn, + PrimaryColumn, + Column, + JoinColumn, +} from 'typeorm'; + +@Entity('discount_redemptions') +export class DiscountRedemption { + @PrimaryColumn() + id: string; + + @Column({ type: 'varchar' }) + discountId: string; + + // User who redeemed the discount + @Column({ type: 'varchar' }) + userId: string; + + @Column({ type: 'varchar', nullable: true }) + contentId: string | null; + + @Column({ type: 'varchar', nullable: true }) + seasonId: string | null; + + // Type of discount at the time of redemption + @Column({ + type: 'varchar', + length: 20, + }) + type: DiscountType; + + // User who created the discount + @Column({ type: 'varchar' }) + discountCreatedById: string; + + // Amount paid at the time of redemption + @Column('decimal', { + precision: 10, + scale: 2, + default: 0, + transformer: { + to: (value) => value, + from: (value) => Number.parseFloat(value), + }, + }) + usdAmount: number; + + // Coupon code used for the redemption + @Column({ type: 'varchar', length: 50 }) + couponCode: string; + + @ManyToOne(() => User, (user) => user.redemptions, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn() + redeemedAt: Date; + + @ManyToOne(() => Discount, (discount) => discount.redemptions, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'discount_id' }) + discount: Discount; +} diff --git a/backend/src/discounts/constants/discount-type.constant.ts b/backend/src/discounts/constants/discount-type.constant.ts new file mode 100644 index 0000000..4a8b4cd --- /dev/null +++ b/backend/src/discounts/constants/discount-type.constant.ts @@ -0,0 +1,8 @@ +export const DiscountTypes = { + PERCENTAGE: 'percentage', + FIXED: 'fixed', + FREE: 'free', +} as const; +export type DiscountType = (typeof DiscountTypes)[keyof typeof DiscountTypes]; +export const DiscountTypeCodes = Object.values(DiscountTypes); +export const DiscountTypeCodesSet = new Set(DiscountTypeCodes); diff --git a/backend/src/discounts/discounts.controller.ts b/backend/src/discounts/discounts.controller.ts new file mode 100644 index 0000000..8780a8b --- /dev/null +++ b/backend/src/discounts/discounts.controller.ts @@ -0,0 +1,88 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, +} from '@nestjs/common'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { DiscountsService } from './discounts.service'; +import { CreateDiscountDto } from './dto/create-discount.dto'; +import { UpdateDiscountDto } from './dto/update-discount.dto'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; + +@Controller('discounts') +export class DiscountsController { + constructor(private readonly discountsService: DiscountsService) {} + + @Post() + @UseGuards(HybridAuthGuard) + create( + @Body() createDiscountDto: CreateDiscountDto, + @User() user: RequestUser['user'], + ) { + return this.discountsService.create(createDiscountDto, user); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.discountsService.findOne(id); + } + + @Patch(':id') + @UseGuards(HybridAuthGuard) + update( + @Param('id') id: string, + @Body() updateDiscountDto: UpdateDiscountDto, + @User() user: RequestUser['user'], + ) { + return this.discountsService.update(id, updateDiscountDto, user); + } + + @Delete(':id') + @UseGuards(HybridAuthGuard) + remove(@Param('id') id: string, @User() user: RequestUser['user']) { + return this.discountsService.remove(id, user); + } + + @Get('/project/:projectId') + @UseGuards(HybridAuthGuard) + findByProjectId( + @Param('projectId') projectId: string, + @User() user: RequestUser['user'], + ) { + return this.discountsService.findByProjectIdAndUser(projectId, user.id); + } + + @Get('/content/:contentId/:couponCode') + @UseGuards(HybridAuthGuard) + findByContentId( + @Param('contentId') contentId: string, + @Param('couponCode') couponCode: string, + @User() user: RequestUser['user'], + ) { + return this.discountsService.findByDiscountCode({ + couponCode: couponCode.toUpperCase(), + contentId, + user, + }); + } + + @Get('/season/:seasonId/:couponCode') + @UseGuards(HybridAuthGuard) + findBySeasonId( + @Param('seasonId') seasonId: string, + @Param('couponCode') couponCode: string, + @User() user: RequestUser['user'], + ) { + return this.discountsService.findByDiscountCode({ + couponCode: couponCode.toUpperCase(), + seasonId, + user, + }); + } +} diff --git a/backend/src/discounts/discounts.module.ts b/backend/src/discounts/discounts.module.ts new file mode 100644 index 0000000..8c3179a --- /dev/null +++ b/backend/src/discounts/discounts.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DiscountsService } from './discounts.service'; +import { DiscountsController } from './discounts.controller'; +import { Discount } from './entities/discount.entity'; +import { Season } from 'src/season/entities/season.entity'; +import { Content } from 'src/contents/entities/content.entity'; +import { Permission } from 'src/projects/entities/permission.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Discount, Season, Content, Permission])], + controllers: [DiscountsController], + providers: [DiscountsService], + exports: [DiscountsService], +}) +export class DiscountsModule {} diff --git a/backend/src/discounts/discounts.service.ts b/backend/src/discounts/discounts.service.ts new file mode 100644 index 0000000..fd4c8f2 --- /dev/null +++ b/backend/src/discounts/discounts.service.ts @@ -0,0 +1,266 @@ +import { + Injectable, + NotFoundException, + UnauthorizedException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Discount } from './entities/discount.entity'; +import { CreateDiscountDto } from './dto/create-discount.dto'; +import { UpdateDiscountDto } from './dto/update-discount.dto'; +import { randomUUID } from 'node:crypto'; +import { Season } from 'src/season/entities/season.entity'; +import { Content } from 'src/contents/entities/content.entity'; +import { User } from 'src/users/entities/user.entity'; +import { Permission } from 'src/projects/entities/permission.entity'; +import { calculateDiscountedPrice } from './utils/calculate-discount-price'; +import { isValidDiscountedPrice } from './utils/is-valid-discount'; + +export interface FindByDiscountCodeProps { + couponCode: string; + contentId?: string; + seasonId?: string; + user: User; +} + +@Injectable() +export class DiscountsService { + constructor( + @InjectRepository(Discount) + private readonly discountRepository: Repository, + @InjectRepository(Season) + private readonly seasonRepository: Repository, + @InjectRepository(Content) + private readonly contentRepository: Repository, + @InjectRepository(Permission) + private readonly permissionRepository: Repository, + ) {} + + async create( + createDiscountDto: CreateDiscountDto, + user: User, + ): Promise { + const userId = user.id; + + const validatedDiscountValue = await this.applyDiscount( + createDiscountDto, + user, + ); + + const discount = this.discountRepository.create({ + id: randomUUID(), + couponCode: createDiscountDto.couponCode, + value: validatedDiscountValue || createDiscountDto.discountValue, + type: createDiscountDto.discountType, + content: { id: createDiscountDto.contentId }, + season: { id: createDiscountDto.seasonId }, + maxUses: + createDiscountDto.discountType === 'free' + ? 1 + : createDiscountDto?.maxUses, + email: createDiscountDto?.email, + createdBy: { id: userId }, + }); + return this.discountRepository.save(discount); + } + + async findAll(): Promise { + return this.discountRepository.find(); + } + + async findOne(id: string): Promise { + const discount = await this.discountRepository.findOne({ where: { id } }); + if (!discount) throw new NotFoundException('Discount not found'); + return discount; + } + + async findByDiscountCode({ + couponCode, + contentId, + seasonId, + user, + }: FindByDiscountCodeProps): Promise { + const discount = await this.discountRepository.findOne({ + where: { couponCode, contentId, seasonId }, + }); + if (discount?.type === 'free' && discount.email !== user.email) { + throw new NotFoundException(); + } + + return discount; + } + + async update( + id: string, + updateDiscountDto: UpdateDiscountDto, + user: User, + ): Promise { + const userId = user.id; + + const validatedDiscountValue = await this.applyDiscount( + updateDiscountDto, + user, + id, + ); + await this.discountRepository.update( + { id, createdById: userId }, + { + couponCode: updateDiscountDto.couponCode, + value: validatedDiscountValue || updateDiscountDto.discountValue, + type: updateDiscountDto.discountType, + contentId: updateDiscountDto.contentId, + seasonId: updateDiscountDto.seasonId, + maxUses: + updateDiscountDto.discountType === 'free' + ? 1 + : updateDiscountDto?.maxUses, + // eslint-disable-next-line unicorn/no-null + email: updateDiscountDto?.email ?? null, + }, + ); + return await this.findOne(id); + } + + async remove(id: string, user: User) { + const deleted = await this.discountRepository.delete({ + createdById: user.id, + id, + }); + if (!deleted.affected) { + throw new NotFoundException(); + } + return { message: 'Discount deleted successfully' }; + } + + async findByProjectIdAndUser( + projectId: string, + userId: string, + ): Promise { + return this.discountRepository + .createQueryBuilder('discount') + .leftJoinAndSelect('discount.content', 'content') + .leftJoinAndSelect('discount.season', 'season') + .where( + '(content.projectId = :projectId OR season.projectId = :projectId)', + { + projectId, + }, + ) + .andWhere('discount.createdBy = :userId', { userId }) + .getMany(); + } + + async applyDiscount( + discountDto: CreateDiscountDto | UpdateDiscountDto, + user: User, + discountId?: string, + ) { + let content: Content; + let season: Season; + let validatedDiscountValue: number; + const userId = user.id; + const filmmakerId = user.filmmaker?.id; + let targetProjectId: string; + + const exists = await this.discountRepository.exists({ + where: { + contentId: discountDto?.contentId, + seasonId: discountDto?.seasonId, + couponCode: discountDto?.couponCode, + }, + }); + + if (exists && !discountId) { + throw new UnprocessableEntityException('Discount code already exists'); + } + + if (discountDto?.contentId) { + content = await this.contentRepository.findOne({ + where: { id: discountDto.contentId }, + }); + + if (!content) { + throw new NotFoundException('Content not found'); + } + + if (!content?.rentalPrice) { + throw new UnprocessableEntityException( + 'Content is not available for rental', + ); + } + + targetProjectId = content.projectId; + } + + if (discountDto?.seasonId) { + season = await this.seasonRepository.findOne({ + where: { id: discountDto.seasonId }, + }); + + if (!season) { + throw new NotFoundException('Season not found'); + } + + if (!season?.rentalPrice) { + throw new UnprocessableEntityException( + 'Season is not available for rental', + ); + } + + targetProjectId = season.projectId; + } + + const ownerPermission = await this.permissionRepository.findOne({ + where: { + filmmakerId: filmmakerId, + projectId: targetProjectId, + role: 'owner', + }, + }); + + if (!ownerPermission) { + throw new UnauthorizedException( + 'User is not authorized to create discounts for this project', + ); + } + + if (discountDto.discountType === 'free') { + const freeDiscountCount = await this.discountRepository.count({ + where: { + type: 'free', + createdById: userId, + seasonId: discountDto?.seasonId, + contentId: discountDto?.contentId, + }, + }); + + if (freeDiscountCount >= 5) { + throw new UnprocessableEntityException( + 'Maximum number of free discounts reached', + ); + } + + if (!discountDto?.email) { + throw new UnprocessableEntityException( + 'Email is required for free discounts', + ); + } + + validatedDiscountValue = content + ? content.rentalPrice + : season.rentalPrice; + } else { + const discountedPrice = calculateDiscountedPrice( + discountDto.discountType, + discountDto.discountValue, + content ? content.rentalPrice : season.rentalPrice, + ); + + if (!isValidDiscountedPrice(discountedPrice, discountDto.discountType)) { + throw new UnprocessableEntityException('Invalid discount'); + } + } + return validatedDiscountValue; + } +} diff --git a/backend/src/discounts/dto/create-discount.dto.ts b/backend/src/discounts/dto/create-discount.dto.ts new file mode 100644 index 0000000..1c8c045 --- /dev/null +++ b/backend/src/discounts/dto/create-discount.dto.ts @@ -0,0 +1,59 @@ +import { + IsString, + IsNumber, + IsOptional, + IsEnum, + IsDateString, + MaxLength, + Min, + IsEmail, +} from 'class-validator'; +import { + DiscountType, + DiscountTypes, +} from '../constants/discount-type.constant'; +import { Transform } from 'class-transformer'; +import * as dayjs from 'dayjs'; + +export class CreateDiscountDto { + @IsString() + @MaxLength(50) + @Transform(({ value }) => value?.toUpperCase()) + couponCode: string; + + @IsNumber() + @Min(0) + discountValue: number; + + @IsEnum(DiscountTypes) + @IsOptional() + discountType?: DiscountType = DiscountTypes.PERCENTAGE; + + @IsOptional() + @IsString() + contentId: string; + + @IsOptional() + @IsString() + seasonId: string; + + @IsOptional() + @IsDateString() + @Transform(({ value }) => value ?? dayjs().toISOString()) + startDate?: string; + + @IsOptional() + @IsDateString() + @Transform(({ value }) => value ?? dayjs().add(1, 'month').toISOString()) + expirationDate?: string; + + @IsOptional() + @IsNumber() + @Min(1) + maxUses?: number; + + @IsOptional() + @IsEmail() + @Transform(({ value }) => value?.toLowerCase()) + email?: string | null; +} diff --git a/backend/src/discounts/dto/request/coupon-code.dto.ts b/backend/src/discounts/dto/request/coupon-code.dto.ts new file mode 100644 index 0000000..407df17 --- /dev/null +++ b/backend/src/discounts/dto/request/coupon-code.dto.ts @@ -0,0 +1,10 @@ +import { IsOptional, IsString, MaxLength } from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class CouponCodeDto { + @IsOptional() + @IsString() + @MaxLength(50) + @Transform(({ value }) => value?.toUpperCase()) + couponCode: string; +} diff --git a/backend/src/discounts/dto/update-discount.dto.ts b/backend/src/discounts/dto/update-discount.dto.ts new file mode 100644 index 0000000..e86dbed --- /dev/null +++ b/backend/src/discounts/dto/update-discount.dto.ts @@ -0,0 +1,6 @@ +import { PartialType, OmitType } from '@nestjs/mapped-types'; +import { CreateDiscountDto } from './create-discount.dto'; + +export class UpdateDiscountDto extends PartialType( + OmitType(CreateDiscountDto, [] as const), +) {} diff --git a/backend/src/discounts/entities/discount.entity.ts b/backend/src/discounts/entities/discount.entity.ts new file mode 100644 index 0000000..a0b4400 --- /dev/null +++ b/backend/src/discounts/entities/discount.entity.ts @@ -0,0 +1,103 @@ +import { Content } from 'src/contents/entities/content.entity'; +import { DiscountRedemption } from 'src/discount-redemption/entities/discount-redemption.entity'; + +import { + Entity, + Column, + ManyToOne, + OneToMany, + CreateDateColumn, + UpdateDateColumn, + JoinColumn, + DeleteDateColumn, + PrimaryColumn, +} from 'typeorm'; +import { + DiscountType, + DiscountTypes, +} from '../constants/discount-type.constant'; +import { User } from 'src/users/entities/user.entity'; +import { Season } from 'src/season/entities/season.entity'; + +@Entity('discounts') +export class Discount { + @PrimaryColumn() + id: string; + + @ManyToOne(() => Content, (content) => content.discounts, { + nullable: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'content_id' }) + content: Content | null; + + @Column({ type: 'varchar', nullable: true }) + contentId: string | null; + + @ManyToOne(() => Season, (season) => season.discounts, { + nullable: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'season_id' }) + season: Season | null; + + @Column({ type: 'varchar', nullable: true }) + seasonId: string | null; + + @Column({ type: 'varchar', length: 50, nullable: false }) + couponCode: string; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + nullable: false, + transformer: { + to: (value) => value, + from: (value) => Number.parseFloat(value), + }, + }) + value: number; + + @Column({ + type: 'varchar', + length: 20, + default: DiscountTypes.PERCENTAGE, + }) + type: DiscountType; + + // Start date (null = no start date) + @Column({ type: 'timestamp', nullable: true }) + startDate: Date | null; + + // End date (null = no expiration date) + @Column({ type: 'timestamp', nullable: true }) + expirationDate: Date | null; + + // Total usage limit (null = unlimited) + @Column({ type: 'int', nullable: true }) + maxUses: number | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + email: string | null; + + @OneToMany(() => DiscountRedemption, (redemption) => redemption.discount) + redemptions: DiscountRedemption[]; + + @ManyToOne(() => User, { nullable: false }) + @JoinColumn({ name: 'created_by_id' }) + createdBy: User; + + @Column({ type: 'varchar' }) + createdById: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + // Soft delete + @DeleteDateColumn() + deletedAt: Date | null; +} diff --git a/backend/src/discounts/utils/calculate-discount-price.ts b/backend/src/discounts/utils/calculate-discount-price.ts new file mode 100644 index 0000000..787f2b2 --- /dev/null +++ b/backend/src/discounts/utils/calculate-discount-price.ts @@ -0,0 +1,16 @@ +import { Discount } from '../entities/discount.entity'; + +export const calculateDiscountedPrice = ( + discountType: Omit, + discountAmount: number, + rentalPrice: number, +) => { + let discountedPrice = 0; + if (discountType === 'fixed') { + discountedPrice = rentalPrice - discountAmount; + } else if (discountType === 'percentage') { + discountedPrice = rentalPrice * (1 - discountAmount / 100); + } + + return discountedPrice; +}; diff --git a/backend/src/discounts/utils/is-valid-discount.ts b/backend/src/discounts/utils/is-valid-discount.ts new file mode 100644 index 0000000..7cfdcb3 --- /dev/null +++ b/backend/src/discounts/utils/is-valid-discount.ts @@ -0,0 +1,9 @@ +import { Discount } from '../entities/discount.entity'; + +export const isValidDiscountedPrice = ( + discountedPrice: number, + discountType: Discount['type'], +) => { + if (discountType === 'free') return true; + return discountedPrice >= 0.5; +}; diff --git a/backend/src/drm/drm.controller.ts b/backend/src/drm/drm.controller.ts new file mode 100644 index 0000000..49f0f41 --- /dev/null +++ b/backend/src/drm/drm.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { DRMService } from './drm.service'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard'; +import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator'; + +@Controller('drm') +export class DRMController { + constructor(private readonly _DRMService: DRMService) {} + + @Get('auth') + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @Subscriptions(['enthusiast', 'film-buff', 'cinephile']) + async authDRM() { + const token = await this._DRMService.getDRMToken(); + return { token }; + } +} diff --git a/backend/src/drm/drm.module.ts b/backend/src/drm/drm.module.ts new file mode 100644 index 0000000..0fd3088 --- /dev/null +++ b/backend/src/drm/drm.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { DRMService } from './drm.service'; +import { DRMController } from './drm.controller'; + +/** + * DRM module stub. + * KeyOS integration removed. DRM token endpoint returns disabled status. + * AES-128 key delivery handled by contents/key.controller.ts instead. + */ +@Module({ + controllers: [DRMController], + providers: [DRMService], + exports: [DRMService], +}) +export class DRMModule {} diff --git a/backend/src/drm/drm.service.ts b/backend/src/drm/drm.service.ts new file mode 100644 index 0000000..355cfdc --- /dev/null +++ b/backend/src/drm/drm.service.ts @@ -0,0 +1,20 @@ +import { Injectable, Logger } from '@nestjs/common'; + +/** + * DRM service stub. + * The original used BuyDRM/KeyOS for Widevine/FairPlay token generation. + * Replaced with a no-op stub. Content protection is now handled by + * AES-128 HLS encryption with a self-hosted key server. + * See: contents/key.controller.ts for the new key delivery system. + */ +@Injectable() +export class DRMService { + private readonly logger = new Logger(DRMService.name); + + async getDRMToken(): Promise { + this.logger.warn( + 'getDRMToken called -- DRM is disabled. Use AES-128 HLS encryption instead.', + ); + return false; + } +} diff --git a/backend/src/events/events.module.ts b/backend/src/events/events.module.ts new file mode 100644 index 0000000..d35e177 --- /dev/null +++ b/backend/src/events/events.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { RentsGateway } from './rents.gateway'; +import { SubscriptionsGateway } from './subscriptions.gateway'; + +@Module({ + providers: [RentsGateway, SubscriptionsGateway], + exports: [RentsGateway, SubscriptionsGateway], +}) +export class EventsModule {} diff --git a/backend/src/events/events.ts b/backend/src/events/events.ts new file mode 100644 index 0000000..cb26f8a --- /dev/null +++ b/backend/src/events/events.ts @@ -0,0 +1,14 @@ +export interface RentsServerToClientEvents { + [rent: string]: (data: { + invoiceId?: string; + rentId?: string; + errorMessage?: string; + }) => void; +} + +export interface SubscriptionServerToClientEvents { + [userId: string]: (data: { + subscriptionId?: string; + errorMessage?: string; + }) => void; +} diff --git a/backend/src/events/rents.gateway.ts b/backend/src/events/rents.gateway.ts new file mode 100644 index 0000000..4a880c7 --- /dev/null +++ b/backend/src/events/rents.gateway.ts @@ -0,0 +1,20 @@ +import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; + +import { Server, Socket } from 'socket.io'; +import { RentsServerToClientEvents } from './events'; +import { SocketAuthMiddleware } from './ws.middleware'; +import { UseGuards } from '@nestjs/common'; +import { WsJwtAuthGuard } from './ws-jwt.guard'; + +@WebSocketGateway({ + cors: true, + namespace: 'events/rents', +}) +@UseGuards(WsJwtAuthGuard) +export class RentsGateway { + @WebSocketServer() server: Server; + + afterInit(client: Socket) { + client.use(SocketAuthMiddleware() as any); + } +} diff --git a/backend/src/events/subscriptions.gateway.ts b/backend/src/events/subscriptions.gateway.ts new file mode 100644 index 0000000..10f9c8f --- /dev/null +++ b/backend/src/events/subscriptions.gateway.ts @@ -0,0 +1,20 @@ +import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; + +import { Server, Socket } from 'socket.io'; +import { SubscriptionServerToClientEvents } from './events'; +import { SocketAuthMiddleware } from './ws.middleware'; +import { UseGuards } from '@nestjs/common'; +import { WsJwtAuthGuard } from './ws-jwt.guard'; + +@WebSocketGateway({ + cors: true, + namespace: 'events/subscriptions', +}) +@UseGuards(WsJwtAuthGuard) +export class SubscriptionsGateway { + @WebSocketServer() server: Server; + + afterInit(client: Socket) { + client.use(SocketAuthMiddleware() as any); + } +} diff --git a/backend/src/events/ws-jwt.guard.ts b/backend/src/events/ws-jwt.guard.ts new file mode 100644 index 0000000..8c5e908 --- /dev/null +++ b/backend/src/events/ws-jwt.guard.ts @@ -0,0 +1,40 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { CognitoJwtVerifier } from 'aws-jwt-verify'; +import { Socket } from 'socket.io'; + +@Injectable() +export class WsJwtAuthGuard implements CanActivate { + static async validateToken(client: Socket) { + const authorization = this.extractTokenFromHeader(client.handshake); + if (!authorization) throw new UnauthorizedException(); + const verifier = CognitoJwtVerifier.create({ + userPoolId: process.env.COGNITO_USER_POOL_ID, + tokenUse: 'id', + clientId: process.env.COGNITO_CLIENT_ID, + }); + + await verifier.verify(authorization); + return true; + } + private static extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } + async canActivate(context: ExecutionContext) { + if (context.getType() !== 'ws') return true; + const client: Socket = context.switchToWs().getClient(); + try { + await WsJwtAuthGuard.validateToken(client); + return true; + } catch (error) { + Logger.error(`Error validating token: ${error.message}`); + throw new UnauthorizedException(); + } + } +} diff --git a/backend/src/events/ws.middleware.ts b/backend/src/events/ws.middleware.ts new file mode 100644 index 0000000..e62b6e5 --- /dev/null +++ b/backend/src/events/ws.middleware.ts @@ -0,0 +1,23 @@ +import { Socket } from 'socket.io'; +import { WsJwtAuthGuard } from './ws-jwt.guard'; + +export type SocketIOMiddleware = ( + client: Socket, + next: (error?: any) => void, +) => void; + +export const SocketAuthMiddleware = (): SocketIOMiddleware => { + return (client, next) => { + WsJwtAuthGuard.validateToken(client) + .then((isValid) => { + if (isValid) { + return next(); + } else { + throw new Error('Unauthorized'); + } + }) + .catch((error) => { + return next(error.message); + }); + }; +}; diff --git a/backend/src/festivals/dto/request/list-festivals.dto.ts b/backend/src/festivals/dto/request/list-festivals.dto.ts new file mode 100644 index 0000000..65aee68 --- /dev/null +++ b/backend/src/festivals/dto/request/list-festivals.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsOptional } from 'class-validator'; + +export class ListFestivalsDTO { + @IsOptional() + @Type(() => String) + search?: string; + + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; +} diff --git a/backend/src/festivals/dto/response/festival.dto.ts b/backend/src/festivals/dto/response/festival.dto.ts new file mode 100644 index 0000000..7c3d2d3 --- /dev/null +++ b/backend/src/festivals/dto/response/festival.dto.ts @@ -0,0 +1,11 @@ +import { Festival } from 'src/festivals/entities/festival.entity'; + +export class FestivalDTO { + id: string; + name: string; + + constructor(festival: Festival) { + this.id = festival.id; + this.name = festival.name; + } +} diff --git a/backend/src/festivals/entities/festival.entity.ts b/backend/src/festivals/entities/festival.entity.ts new file mode 100644 index 0000000..924e38f --- /dev/null +++ b/backend/src/festivals/entities/festival.entity.ts @@ -0,0 +1,27 @@ +import { FestivalScreening } from 'src/projects/entities/festival-screening.entity'; +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('festivals') +export class Festival { + @PrimaryColumn() + id: string; + + @Column() + name: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @OneToMany(() => FestivalScreening, (screening) => screening.festival) + screenings: FestivalScreening[]; +} diff --git a/backend/src/festivals/festivals.controller.ts b/backend/src/festivals/festivals.controller.ts new file mode 100644 index 0000000..6788143 --- /dev/null +++ b/backend/src/festivals/festivals.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; + +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { FestivalsService } from './festivals.service'; +import { FestivalDTO } from './dto/response/festival.dto'; +import { ListFestivalsDTO } from './dto/request/list-festivals.dto'; + +@Controller('festivals') +@UseGuards(HybridAuthGuard) +export class FestivalsController { + constructor(private readonly festivalsService: FestivalsService) {} + + @Get() + async findAll(@Query() query: ListFestivalsDTO) { + const festivals = await this.festivalsService.findAll(query); + return festivals.map((festival) => new FestivalDTO(festival)); + } + + @Get('count') + async getCount(@Query() query: ListFestivalsDTO) { + const total = await this.festivalsService.getCount(query); + return { count: total }; + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const festival = await this.festivalsService.findOne(id); + return festival; + } +} diff --git a/backend/src/festivals/festivals.module.ts b/backend/src/festivals/festivals.module.ts new file mode 100644 index 0000000..fc3007e --- /dev/null +++ b/backend/src/festivals/festivals.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Festival } from './entities/festival.entity'; +import { FestivalsController } from './festivals.controller'; +import { FestivalsService } from './festivals.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Festival])], + controllers: [FestivalsController], + providers: [FestivalsService], +}) +export class FestivalsModule {} diff --git a/backend/src/festivals/festivals.service.ts b/backend/src/festivals/festivals.service.ts new file mode 100644 index 0000000..66d6361 --- /dev/null +++ b/backend/src/festivals/festivals.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Festival } from './entities/festival.entity'; +import { ListFestivalsDTO } from './dto/request/list-festivals.dto'; + +@Injectable() +export class FestivalsService { + constructor( + @InjectRepository(Festival) + private festivalsRepository: Repository, + ) {} + + findAll(query: ListFestivalsDTO) { + return this.getFestivalsQuery(query) + .orderBy('festival.name', 'ASC') + .limit(query.limit) + .offset(query.offset) + .getMany(); + } + + getCount(query: ListFestivalsDTO) { + return this.getFestivalsQuery(query).getCount(); + } + + getFestivalsQuery(query: ListFestivalsDTO) { + const databaseQuery = + this.festivalsRepository.createQueryBuilder('festival'); + if (query.search) { + databaseQuery.where('festival.name ILIKE :search', { + search: `%${query.search}%`, + }); + } + return databaseQuery; + } + + findOne(id: string) { + return this.festivalsRepository.findOne({ + where: { id }, + }); + } +} diff --git a/backend/src/ffmpeg-worker/worker.ts b/backend/src/ffmpeg-worker/worker.ts new file mode 100644 index 0000000..505e232 --- /dev/null +++ b/backend/src/ffmpeg-worker/worker.ts @@ -0,0 +1,191 @@ +/** + * FFmpeg Transcoding Worker + * + * Standalone BullMQ consumer that processes video transcoding jobs. + * Runs in a separate Docker container with FFmpeg installed. + * + * Pipeline: + * 1. Download raw video from MinIO + * 2. Generate AES-128 encryption key + * 3. Transcode to HLS with multiple quality levels + * 4. Encrypt segments with AES-128 + * 5. Upload HLS output to MinIO + * 6. Store encryption key in PostgreSQL + * 7. Update API with job status + */ + +import { Worker, Job } from 'bullmq'; +import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; +import { randomBytes, randomUUID } from 'node:crypto'; +import { execSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { Client } from 'pg'; + +interface TranscodeJobData { + contentId: string; + sourceKey: string; + outputPrefix: string; + bucket?: string; +} + +const s3 = new S3Client({ + region: process.env.AWS_REGION || 'us-east-1', + endpoint: process.env.S3_ENDPOINT || 'http://minio:9000', + forcePathStyle: true, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY || 'minioadmin', + secretAccessKey: process.env.AWS_SECRET_KEY || 'minioadmin123', + }, +}); + +const privateBucket = process.env.S3_PRIVATE_BUCKET_NAME || 'indeedhub-private'; + +async function downloadFromS3(key: string, destPath: string) { + const command = new GetObjectCommand({ Bucket: privateBucket, Key: key }); + const response = await s3.send(command); + const chunks: Buffer[] = []; + for await (const chunk of response.Body as any) { + chunks.push(chunk); + } + writeFileSync(destPath, Buffer.concat(chunks)); +} + +async function uploadToS3(filePath: string, key: string, contentType: string) { + const body = readFileSync(filePath); + const command = new PutObjectCommand({ + Bucket: privateBucket, + Key: key, + Body: body, + ContentType: contentType, + }); + await s3.send(command); +} + +async function storeKey(contentId: string, keyData: Buffer, iv: Buffer) { + const client = new Client({ + host: process.env.DATABASE_HOST || 'postgres', + port: Number(process.env.DATABASE_PORT || '5432'), + user: process.env.DATABASE_USER || 'indeedhub', + password: process.env.DATABASE_PASSWORD || 'indeedhub_dev_2026', + database: process.env.DATABASE_NAME || 'indeedhub', + }); + + await client.connect(); + try { + await client.query( + `INSERT INTO content_keys (id, content_id, key_data, iv, rotation_index, created_at) + VALUES ($1, $2, $3, $4, 0, NOW()) + ON CONFLICT (id) DO NOTHING`, + [randomUUID(), contentId, keyData, iv], + ); + } finally { + await client.end(); + } +} + +async function processJob(job: Job) { + const { contentId, sourceKey, outputPrefix } = job.data; + const workDir = mkdtempSync(join(tmpdir(), 'transcode-')); + + try { + console.log(`[transcode] Starting job for content: ${contentId}`); + await job.updateProgress(5); + + // Step 1: Download source video + const sourcePath = join(workDir, 'source.mp4'); + console.log(`[transcode] Downloading ${sourceKey}...`); + await downloadFromS3(sourceKey, sourcePath); + await job.updateProgress(20); + + // Step 2: Generate AES-128 key and IV + const keyData = randomBytes(16); + const iv = randomBytes(16); + const keyPath = join(workDir, 'enc.key'); + const ivHex = iv.toString('hex'); + writeFileSync(keyPath, keyData); + + // Step 3: Create key info file for FFmpeg + // Format: key_uri\nkey_file_path\niv + const keyInfoPath = join(workDir, 'enc.keyinfo'); + // The URI that players will use to fetch the key + const keyUri = `/api/contents/${contentId}/key`; + writeFileSync(keyInfoPath, `${keyUri}\n${keyPath}\n${ivHex}\n`); + + await job.updateProgress(25); + + // Step 4: Transcode to HLS with AES-128 encryption + console.log(`[transcode] Transcoding with FFmpeg...`); + const hlsDir = join(workDir, 'hls'); + execSync(`mkdir -p ${hlsDir}`); + + // Multi-quality HLS with encryption + const ffmpegCmd = [ + 'ffmpeg', '-i', sourcePath, + '-hide_banner', '-loglevel', 'warning', + // 720p rendition + '-map', '0:v:0', '-map', '0:a:0', + '-c:v', 'libx264', '-preset', 'medium', '-crf', '23', + '-vf', 'scale=-2:720', + '-c:a', 'aac', '-b:a', '128k', + '-hls_time', '6', + '-hls_list_size', '0', + '-hls_key_info_file', keyInfoPath, + '-hls_segment_filename', join(hlsDir, 'seg_%03d.ts'), + '-f', 'hls', + join(hlsDir, 'index.m3u8'), + ].join(' '); + + execSync(ffmpegCmd, { stdio: 'pipe', timeout: 3600_000 }); + await job.updateProgress(75); + + // Step 5: Upload HLS output to MinIO + console.log(`[transcode] Uploading HLS segments to MinIO...`); + const files = readdirSync(hlsDir); + for (const file of files) { + const filePath = join(hlsDir, file); + const s3Key = `${outputPrefix}/${file}`; + const ct = file.endsWith('.m3u8') + ? 'application/vnd.apple.mpegurl' + : 'video/MP2T'; + await uploadToS3(filePath, s3Key, ct); + } + await job.updateProgress(90); + + // Step 6: Store encryption key in database + console.log(`[transcode] Storing encryption key...`); + await storeKey(contentId, keyData, iv); + + await job.updateProgress(100); + console.log(`[transcode] Job complete for content: ${contentId}`); + + return { + manifestKey: `${outputPrefix}/index.m3u8`, + segments: files.length, + }; + } finally { + // Clean up temp directory + rmSync(workDir, { recursive: true, force: true }); + } +} + +// ── Start the BullMQ worker ────────────────────────────────── +const worker = new Worker('transcode', processJob, { + connection: { + host: process.env.QUEUE_HOST || 'redis', + port: Number(process.env.QUEUE_PORT || '6379'), + password: process.env.QUEUE_PASSWORD || undefined, + }, + concurrency: 1, +}); + +worker.on('completed', (job) => { + console.log(`[worker] Job ${job.id} completed`); +}); + +worker.on('failed', (job, err) => { + console.error(`[worker] Job ${job?.id} failed:`, err.message); +}); + +console.log('[worker] FFmpeg transcoding worker started, waiting for jobs...'); diff --git a/backend/src/filmmakers/dto/request/analytics.dto.ts b/backend/src/filmmakers/dto/request/analytics.dto.ts new file mode 100644 index 0000000..27eef50 --- /dev/null +++ b/backend/src/filmmakers/dto/request/analytics.dto.ts @@ -0,0 +1,20 @@ +import { Transform } from 'class-transformer'; +import { IsOptional, IsString } from 'class-validator'; +import { FilterDateRange } from 'src/common/enums/filter-date-range.enum'; + +export class FilmmakerAnalyticsDTO { + @IsOptional() + @Transform(({ value }) => { + if (value === undefined || value === null || value === '') { + return []; + } + + return Array.isArray(value) ? value : [value]; + }) + @IsString({ each: true }) + projectIds: string[] = []; + + @IsOptional() + @IsString() + dateRange: FilterDateRange = 'since_uploaded'; +} diff --git a/backend/src/filmmakers/dto/request/create-filmmaker.dto.ts b/backend/src/filmmakers/dto/request/create-filmmaker.dto.ts new file mode 100644 index 0000000..dcb0597 --- /dev/null +++ b/backend/src/filmmakers/dto/request/create-filmmaker.dto.ts @@ -0,0 +1,15 @@ +import { IsOptional, IsString, MaxLength } from 'class-validator'; + +export class CreateFilmmakerDTO { + @IsString() + @MaxLength(100) + professionalName: string; + + @IsString() + @MaxLength(150) + @IsOptional() + bio?: string; + + @IsString() + userId: string; +} diff --git a/backend/src/filmmakers/dto/request/list-filmmakers.dto.ts b/backend/src/filmmakers/dto/request/list-filmmakers.dto.ts new file mode 100644 index 0000000..2c9c4ad --- /dev/null +++ b/backend/src/filmmakers/dto/request/list-filmmakers.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsOptional } from 'class-validator'; + +export class ListFilmmakersDTO { + @IsOptional() + @Type(() => String) + search?: string; + + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; +} diff --git a/backend/src/filmmakers/dto/request/update-filmmaker.dto.ts b/backend/src/filmmakers/dto/request/update-filmmaker.dto.ts new file mode 100644 index 0000000..a03ff80 --- /dev/null +++ b/backend/src/filmmakers/dto/request/update-filmmaker.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateFilmmakerDTO } from './create-filmmaker.dto'; + +export class UpdateFilmmakerDTO extends PartialType(CreateFilmmakerDTO) {} diff --git a/backend/src/filmmakers/dto/response/filmmaker.dto.ts b/backend/src/filmmakers/dto/response/filmmaker.dto.ts new file mode 100644 index 0000000..b18397e --- /dev/null +++ b/backend/src/filmmakers/dto/response/filmmaker.dto.ts @@ -0,0 +1,25 @@ +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; + +export class FilmmakerDTO { + id: string; + legalName: string; + professionalName: string; + email?: string; + bio?: string; + profilePictureUrl?: string; + + constructor(filmmaker?: Filmmaker) { + if (filmmaker) { + this.id = filmmaker.id; + this.professionalName = filmmaker.professionalName; + this.bio = filmmaker.bio; + if (filmmaker.user) { + this.legalName = filmmaker.user.legalName; + this.email = filmmaker.user.email; + if (filmmaker.user.profilePictureUrl) + this.profilePictureUrl = + process.env.S3_PUBLIC_BUCKET_URL + filmmaker.user.profilePictureUrl; + } + } + } +} diff --git a/backend/src/filmmakers/entities/filmmaker.entity.ts b/backend/src/filmmakers/entities/filmmaker.entity.ts new file mode 100644 index 0000000..b3a7298 --- /dev/null +++ b/backend/src/filmmakers/entities/filmmaker.entity.ts @@ -0,0 +1,62 @@ +import { MaxLength } from 'class-validator'; +import { Cast } from 'src/contents/entities/cast.entity'; +import { Crew } from 'src/contents/entities/crew.entity'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { PaymentMethod } from 'src/payment/entities/payment-method.entity'; +import { Permission } from 'src/projects/entities/permission.entity'; +import { User } from 'src/users/entities/user.entity'; +import { + PrimaryColumn, + Column, + UpdateDateColumn, + CreateDateColumn, + OneToOne, + JoinColumn, + Entity, + OneToMany, + DeleteDateColumn, +} from 'typeorm'; + +@Entity('filmmakers') +export class Filmmaker { + @PrimaryColumn() + id: string; + + @Column() + professionalName: string; + + @Column({ nullable: true }) + @MaxLength(500) + bio: string; + + @Column() + userId: string; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @OneToOne(() => User, (user) => user.filmmaker) + @JoinColumn({ name: 'user_id' }) + user?: User; + + @OneToMany(() => PaymentMethod, (paymentMethod) => paymentMethod.filmmaker) + paymentMethods?: PaymentMethod[]; + + @OneToMany(() => Shareholder, (shareholder) => shareholder.filmmaker) + shareholderFilms?: Shareholder[]; + + @OneToMany(() => Cast, (cast) => cast.filmmaker) + castFilms?: Cast[]; + + @OneToMany(() => Crew, (crew) => crew.filmmaker) + crewFilms?: Crew[]; + + @OneToMany(() => Permission, (permission) => permission.filmmaker) + films?: Permission[]; +} diff --git a/backend/src/filmmakers/filmmakers.controller.ts b/backend/src/filmmakers/filmmakers.controller.ts new file mode 100644 index 0000000..4ebdc18 --- /dev/null +++ b/backend/src/filmmakers/filmmakers.controller.ts @@ -0,0 +1,72 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { FilmmakersService } from './filmmakers.service'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { ListFilmmakersDTO } from './dto/request/list-filmmakers.dto'; +import { FilmmakerDTO } from './dto/response/filmmaker.dto'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { FilmmakerAnalyticsDTO } from './dto/request/analytics.dto'; + +@Controller('filmmakers') +export class FilmmakersController { + constructor(private readonly filmmakersService: FilmmakersService) {} + + // Commented out because we don't want to allow anyone to create a filmmaker + // @Post() + // create(@Body() createFilmmakerDTO: CreateFilmmakerDTO) { + // return this.filmmakersService.create(createFilmmakerDTO); + // } + + @Get() + async findAll(@Query() query: ListFilmmakersDTO) { + const filmmakers = await this.filmmakersService.findAll(query); + return filmmakers.map((fm) => new FilmmakerDTO(fm)); + } + + @Get('/count') + async findAllCount(@Query() query: ListFilmmakersDTO) { + const total = await this.filmmakersService.getCount(query); + return { count: total }; + } + + @Get('/analytics') + @UseGuards(HybridAuthGuard) + async getAnalytics(@User() user: RequestUser['user']) { + return this.filmmakersService.getFilmmakerAnalytics(user.filmmaker.id); + } + + @Get('/watch-analytics') + @UseGuards(HybridAuthGuard) + async watchAnalytics( + @User() user: RequestUser['user'], + @Query() query: FilmmakerAnalyticsDTO, + ) { + return this.filmmakersService.getFilmmakerWatchAnalytics( + user.filmmaker.id, + query, + ); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const fm = await this.filmmakersService.findOne(id); + return new FilmmakerDTO(fm); + } + + @Get('/project/:id/owner') + async findOwnerByProjectId(@Param('id') id: string) { + const fm = await this.filmmakersService.findOwnerByProjectId(id); + return new FilmmakerDTO(fm); + } + + @Get(':id/lightning-address') + async getLightningAddress(@Param('id') id: string) { + return await this.filmmakersService.getFilmmakerLightningAddress(id); + } + + // Commented out because we don't want to allow anyone to delete a filmmaker + // @Delete(':id') + // remove(@Param('id') id: string) { + // return this.filmmakersService.remove(+id); + // } +} diff --git a/backend/src/filmmakers/filmmakers.module.ts b/backend/src/filmmakers/filmmakers.module.ts new file mode 100644 index 0000000..2920f71 --- /dev/null +++ b/backend/src/filmmakers/filmmakers.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { FilmmakersService } from './filmmakers.service'; +import { FilmmakersController } from './filmmakers.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Filmmaker } from './entities/filmmaker.entity'; +import { Rent } from 'src/rents/entities/rent.entity'; +import { User } from 'src/auth/user.decorator'; +import { InvitesModule } from 'src/invites/invites.module'; +import { UsersModule } from 'src/users/users.module'; +import { ContentsModule } from 'src/contents/contents.module'; +import { ProjectsModule } from 'src/projects/projects.module'; +import { PostHogModule } from 'src/posthog/posthog.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Filmmaker, User, Rent]), + InvitesModule, + UsersModule, + ContentsModule, + ProjectsModule, + PostHogModule, + ], + controllers: [FilmmakersController], + providers: [FilmmakersService], + exports: [FilmmakersService], +}) +export class FilmmakersModule {} diff --git a/backend/src/filmmakers/filmmakers.service.ts b/backend/src/filmmakers/filmmakers.service.ts new file mode 100644 index 0000000..c6db807 --- /dev/null +++ b/backend/src/filmmakers/filmmakers.service.ts @@ -0,0 +1,364 @@ +import { + Inject, + Injectable, + NotFoundException, + forwardRef, +} from '@nestjs/common'; +import { CreateFilmmakerDTO } from './dto/request/create-filmmaker.dto'; +import { UpdateFilmmakerDTO } from './dto/request/update-filmmaker.dto'; +import { Filmmaker } from './entities/filmmaker.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Brackets, Repository } from 'typeorm'; +import { Rent } from 'src/rents/entities/rent.entity'; +import { randomUUID } from 'node:crypto'; +import { ListFilmmakersDTO } from './dto/request/list-filmmakers.dto'; +import { InvitesService } from 'src/invites/invites.service'; +import { UsersService } from 'src/users/users.service'; +import { User } from 'src/users/entities/user.entity'; +import { PRICE_PER_SECOND } from 'src/common/constants'; +import { ContentsService } from 'src/contents/contents.service'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { ProjectsService } from 'src/projects/projects.service'; +import { fullRelations } from 'src/projects/entities/project.entity'; +import { FilmmakerAnalyticsDTO } from './dto/request/analytics.dto'; +import { PosthogService } from 'src/posthog/posthog.service'; +import { getFilterDateRange } from 'src/common/helper'; + +@Injectable() +export class FilmmakersService { + constructor( + @InjectRepository(Filmmaker) + private filmmakerRepository: Repository, + @Inject(InvitesService) + private invitesService: InvitesService, + @Inject(ContentsService) + private contentsService: ContentsService, + @Inject(ProjectsService) + private projectsService: ProjectsService, + @Inject(PosthogService) + private posthogService: PosthogService, + @Inject(forwardRef(() => UsersService)) + private usersService: UsersService, + @InjectRepository(Rent) + private rentRepository: Repository, + ) {} + + async create(createFilmmakerDTO: CreateFilmmakerDTO) { + const filmmaker = this.filmmakerRepository.create({ + id: randomUUID(), + ...createFilmmakerDTO, + }); + const saved = await this.filmmakerRepository.save(filmmaker); + + const user = await this.usersService.findUsersById(saved.userId); + await this.checkForShareholderInvites(user, saved.id); + await this.projectsService.updateOnRegister(user.email, saved.id); + await this.contentsService.updateOnRegister(user.email, saved.id); + return saved; + } + + async checkForShareholderInvites(user: User, filmmakerId: string) { + const invites = await this.invitesService.getUserInvites(user.email); + if (invites) { + for (const invite of invites) { + await this.contentsService.addInvitedFilmmaker(filmmakerId, invite); + } + } + await this.invitesService.removeInvite(user.email); + } + + findAll(query: ListFilmmakersDTO) { + const fmQuery = this.getFilmmakersQuery(query); + + fmQuery.skip(query.offset); + fmQuery.take(query.limit); + return fmQuery.getMany(); + } + + getCount(query: ListFilmmakersDTO) { + const filmsQuery = this.getFilmmakersQuery(query); + return filmsQuery.getCount(); + } + + async findOne(id: string) { + try { + const fm = await this.filmmakerRepository.findOneOrFail({ + where: { id }, + relations: ['user'], + }); + return fm; + } catch { + throw new NotFoundException('Filmmaker not found'); + } + } + + async findOwnerByProjectId(projectId: string) { + try { + const project = await this.projectsService.findOne( + projectId, + fullRelations, + ); + // Find the permission with the 'owner' role + const ownerPermission = project.permissions.find( + (permission) => permission.role === 'owner', + ); + + return ownerPermission.filmmaker; // Return the filmmaker associated with the 'owner' role + } catch { + throw new NotFoundException('Filmmaker not found'); + } + } + + async update(id: string, updateFilmmakerDTO: UpdateFilmmakerDTO) { + const fm = await this.filmmakerRepository.update(id, updateFilmmakerDTO); + return { success: fm.affected > 0 }; + } + + async delete(id: string) { + const film = await this.filmmakerRepository.softDelete(id); + const projects = await this.projectsService.findAll({ + filmmakerId: id, + limit: undefined, + offset: 0, + show: 'owner', + }); + for (const project of projects) { + await this.projectsService.remove(project.id); + await this.projectsService.removeFilmmaker(id); + } + await this.contentsService.removeFilmmaker(id); + return { success: film.affected > 0 }; + } + + getFilmmakersQuery(query: ListFilmmakersDTO) { + const fmQuery = this.filmmakerRepository.createQueryBuilder('filmmakers'); + fmQuery.leftJoinAndSelect('filmmakers.user', 'user'); + + if (query.search) { + fmQuery.andWhere( + new Brackets((qb) => { + qb.where('user.email ILIKE :email', { + email: `%${query.search}%`, + }).orWhere('filmmakers.professional_name ILIKE :name', { + name: `%${query.search}%`, + }); + }), + ); + } + return fmQuery; + } + + async getFilmmakerAnalytics(filmmakerId: string) { + const filmmaker = await this.filmmakerRepository.findOneOrFail({ + where: { id: filmmakerId }, + relations: ['shareholderFilms'], + }); + + let balance = 0; + const shareholders: Shareholder[] = []; + + for (const shareholder of filmmaker.shareholderFilms) { + if (shareholder.filmmakerId === filmmakerId) { + balance += Number(shareholder.pendingRevenue); + balance += Number(shareholder.rentPendingRevenue); + shareholders.push(shareholder); + } + } + + const { usd, milisat } = await this.contentsService.getShareholdersTotal( + shareholders.map((s) => s.id), + ); + + return { + balance: { + usd: balance, + milisat: balance / PRICE_PER_SECOND, + }, + total: { + usd, + milisat, + }, + }; + } + + async getFilmmakerWatchAnalytics( + filmmakerId: string, + { projectIds, dateRange }: FilmmakerAnalyticsDTO, + ) { + // Normalize project ids and fall back to all filmmaker projects when missing/empty + let ids = (projectIds ?? []).filter((id) => !!id); + let allProjects = []; + + if (ids.length === 0) { + allProjects = await this.projectsService.findAll({ + filmmakerId: filmmakerId, + limit: undefined, + offset: 0, + show: 'shareholder', + }); + ids = allProjects.map((project) => project.id).filter(Boolean); + } + + // No projects at all — return empty analytics instead of querying with IN () + if (ids.length === 0) { + return { + viewsByDate: [], + trailerViews: 0, + averageWatchTime: 0, + streamingRevenueSats: 0, + rentalRevenueSats: 0, + purchasesCount: 0, + purchasesByContent: [], + revenueByDate: [], + }; + } + + const { startDate, endDate } = getFilterDateRange(dateRange); + + const viewsByDate = await this.posthogService.getViewsByProjectIds( + ids, + startDate, + endDate, + ); + + const trailerViews = + await this.posthogService.getTrailerViewsByProjectIds(ids); + + const averageWatchTimeByDate = + await this.posthogService.getAverageWatchTimeByProjectIds( + ids, + startDate, + endDate, + ); + + // Calculate overall average watch time + let totalAvgWatchTime = 0; + if (averageWatchTimeByDate.length > 0) { + const sumAvg = averageWatchTimeByDate.reduce( + (accumulator, current) => + accumulator + current.averageWatchtTimePerUser, + 0, + ); + totalAvgWatchTime = sumAvg / averageWatchTimeByDate.length; + } + + const revenueByDate = await this.posthogService.getSatoshisByDateProjectIds( + ids, + startDate, + endDate, + ); + + const streamingRevenueSats = revenueByDate.reduce( + (accumulator, current) => accumulator + current.totalSatAmount, + 0, + ); + + // Calculate average sat price — the stub service doesn't provide it, + // so we default to 0 (no USD conversion until real analytics are wired). + const averageSatPrice = 0; + + // Filter rents by projectIds if we couldn't do it in the find (if ids.length > 1) + // Actually, let's use QueryBuilder to be safe and efficient + const rentsQuery = this.rentRepository.createQueryBuilder('rent'); + rentsQuery.leftJoinAndSelect('rent.content', 'content'); + rentsQuery.leftJoinAndSelect('content.project', 'project'); + rentsQuery.where('rent.status = :status', { status: 'paid' }); + rentsQuery.andWhere('rent.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }); + rentsQuery.andWhere('content.projectId IN (:...ids)', { ids }); + + const rentsResult = await rentsQuery.getMany(); + + const purchasesCount = rentsResult.length; + let rentalRevenueUsd = 0; + const purchasesByContentMap = new Map(); + const rentalRevenueByDateMap = new Map(); + + for (const rent of rentsResult) { + rentalRevenueUsd += Number(rent.usdAmount); + const title = rent.content.title || rent.content.project.title; + purchasesByContentMap.set( + title, + (purchasesByContentMap.get(title) || 0) + 1, + ); + + const date = rent.createdAt.toISOString().split('T')[0]; + rentalRevenueByDateMap.set( + date, + (rentalRevenueByDateMap.get(date) || 0) + Number(rent.usdAmount), + ); + } + + const purchasesByContent = [...purchasesByContentMap.entries()].map( + ([title, count]) => ({ title, count }), + ); + + const rentalRevenueSats = + averageSatPrice > 0 ? rentalRevenueUsd / averageSatPrice : 0; + + // Merge revenue by date + const revenueByDateMap = new Map< + string, + { streaming: number; rental: number; date: string } + >(); + + // Initialize with streaming data + for (const item of revenueByDate) { + const date = new Date(item.date).toISOString().split('T')[0]; + revenueByDateMap.set(date, { + date: new Date(item.date).toISOString().split('T')[0], + streaming: item.totalSatAmount, + rental: 0, + }); + } + + // Add rental data + for (const [date, usdAmount] of rentalRevenueByDateMap.entries()) { + const sats = averageSatPrice > 0 ? usdAmount / averageSatPrice : 0; + if (revenueByDateMap.has(date)) { + const entry = revenueByDateMap.get(date); + entry.rental = sats; + } else { + revenueByDateMap.set(date, { + date, + streaming: 0, + rental: sats, + }); + } + } + + const revenueBreakdownByDate = [...revenueByDateMap.values()].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + + return { + viewsByDate, + trailerViews, + averageWatchTime: totalAvgWatchTime, + streamingRevenueSats, + rentalRevenueSats, + purchasesCount, + purchasesByContent, + revenueByDate: revenueBreakdownByDate, + }; + } + + async getFilmmakerLightningAddress(filmmakerId: string) { + const filmmaker = await this.filmmakerRepository.findOneOrFail({ + where: { id: filmmakerId }, + relations: ['paymentMethods'], + }); + + const paymentMethod = filmmaker.paymentMethods.find( + (pm) => pm.selected, + )?.lightningAddress; + + if (paymentMethod) { + return { lightningAddress: paymentMethod }; + } else { + throw new NotFoundException('No lightning address found'); + } + } +} diff --git a/backend/src/genres/dto/request/list-genres.dto.ts b/backend/src/genres/dto/request/list-genres.dto.ts new file mode 100644 index 0000000..2575504 --- /dev/null +++ b/backend/src/genres/dto/request/list-genres.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEnum, IsOptional } from 'class-validator'; +import { + Type as ProjectType, + types as projectTypes, +} from 'src/projects/enums/type.enum'; + +export class ListGenresDTO { + @IsOptional() + @Type(() => String) + search?: string; + + @IsOptional() + @IsEnum(projectTypes) + type?: ProjectType; + + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; + + @ApiProperty() + @IsOptional() + subgenres = false; +} diff --git a/backend/src/genres/dto/response/genre.dto.ts b/backend/src/genres/dto/response/genre.dto.ts new file mode 100644 index 0000000..2931d2d --- /dev/null +++ b/backend/src/genres/dto/response/genre.dto.ts @@ -0,0 +1,21 @@ +import { Genre } from 'src/genres/entities/genre.entity'; +import { SubgenreDTO } from './subgenre.dto'; +import { Type } from 'src/projects/enums/type.enum'; + +export class GenreDTO { + id: string; + name: string; + subgenres?: SubgenreDTO[]; + type: Type; + + constructor(genre: Genre) { + this.id = genre.id; + this.name = genre.name; + this.type = genre.type; + if (genre.subgenres) { + this.subgenres = genre.subgenres.map( + (subgenre) => new SubgenreDTO(subgenre), + ); + } + } +} diff --git a/backend/src/genres/dto/response/subgenre.dto.ts b/backend/src/genres/dto/response/subgenre.dto.ts new file mode 100644 index 0000000..4e3d80d --- /dev/null +++ b/backend/src/genres/dto/response/subgenre.dto.ts @@ -0,0 +1,14 @@ +import { GenreDTO } from './genre.dto'; +import { Subgenre } from 'src/genres/entities/subgenre.entity'; + +export class SubgenreDTO { + id: string; + name: string; + genre?: GenreDTO; + + constructor(subgenre: Subgenre) { + this.id = subgenre.id; + this.name = subgenre.name; + if (subgenre.genre) this.genre = new GenreDTO(subgenre.genre); + } +} diff --git a/backend/src/genres/entities/genre.entity.ts b/backend/src/genres/entities/genre.entity.ts new file mode 100644 index 0000000..e1542f7 --- /dev/null +++ b/backend/src/genres/entities/genre.entity.ts @@ -0,0 +1,35 @@ +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Subgenre } from './subgenre.entity'; +import { Type } from 'src/projects/enums/type.enum'; +import { Project } from 'src/projects/entities/project.entity'; + +@Entity('genres') +export class Genre { + @PrimaryColumn() + id: string; + + @Column() + name: string; + + @Column({ default: 'film' }) + type: Type; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @OneToMany(() => Subgenre, (subgenre) => subgenre.genre) + subgenres: Subgenre[]; + + @OneToMany(() => Project, (project) => project.genre) + projects: Project[]; +} diff --git a/backend/src/genres/entities/subgenre.entity.ts b/backend/src/genres/entities/subgenre.entity.ts new file mode 100644 index 0000000..e1255bc --- /dev/null +++ b/backend/src/genres/entities/subgenre.entity.ts @@ -0,0 +1,35 @@ +import { ProjectGenre } from 'src/projects/entities/project-genre.entity'; +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + OneToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Genre } from './genre.entity'; + +@Entity('subgenres') +export class Subgenre { + @PrimaryColumn() + id: string; + + @Column() + name: string; + + @Column() + genreId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @OneToMany(() => ProjectGenre, (projectGenre) => projectGenre.subgenre) + projects: ProjectGenre[]; + + @ManyToOne(() => Genre, (genre) => genre.subgenres) + genre: Genre; +} diff --git a/backend/src/genres/genres.controller.ts b/backend/src/genres/genres.controller.ts new file mode 100644 index 0000000..cf49a27 --- /dev/null +++ b/backend/src/genres/genres.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Query } from '@nestjs/common'; + +import { GenresService } from './genres.service'; +import { GenreDTO } from './dto/response/genre.dto'; +import { ListGenresDTO } from './dto/request/list-genres.dto'; +import { CacheTTL } from '@nestjs/cache-manager'; + +@Controller('genres') +@CacheTTL(60 * 60) +export class GenresController { + constructor(private readonly genresService: GenresService) {} + + @Get() + async findAll(@Query() query: ListGenresDTO) { + const genres = await this.genresService.findAll(query); + return genres.map((genre) => new GenreDTO(genre)); + } + + @Get('/count') + async findAllCount(@Query() query: ListGenresDTO) { + const total = await this.genresService.getCount(query); + return { count: total }; + } +} diff --git a/backend/src/genres/genres.module.ts b/backend/src/genres/genres.module.ts new file mode 100644 index 0000000..8d01e40 --- /dev/null +++ b/backend/src/genres/genres.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Genre } from './entities/genre.entity'; +import { GenresController } from './genres.controller'; +import { GenresService } from './genres.service'; +import { Subgenre } from './entities/subgenre.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Genre, Subgenre])], + controllers: [GenresController], + providers: [GenresService], +}) +export class GenresModule {} diff --git a/backend/src/genres/genres.service.ts b/backend/src/genres/genres.service.ts new file mode 100644 index 0000000..d2d7e64 --- /dev/null +++ b/backend/src/genres/genres.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Genre } from './entities/genre.entity'; +import { ListGenresDTO } from './dto/request/list-genres.dto'; + +@Injectable() +export class GenresService { + constructor( + @InjectRepository(Genre) + private genresRepository: Repository, + ) {} + + findAll(query: ListGenresDTO) { + return this.getGenresQuery(query) + .limit(query.limit) + .offset(query.offset) + .getMany(); + } + + getCount(query: ListGenresDTO) { + return this.getGenresQuery(query).getCount(); + } + + getGenresQuery(query: ListGenresDTO) { + const databaseQuery = this.genresRepository.createQueryBuilder('genre'); + databaseQuery.distinct(true); + if (query.search) { + databaseQuery.where('genre.name ILIKE :search', { + search: `%${query.search}%`, + }); + } + if (query.type) { + databaseQuery.andWhere('genre.type = :type', { type: query.type }); + } + + if (query.subgenres) { + databaseQuery.leftJoinAndSelect('genre.subgenres', 'subgenre'); + } + return databaseQuery; + } +} diff --git a/backend/src/instrument.ts b/backend/src/instrument.ts new file mode 100644 index 0000000..6b35b86 --- /dev/null +++ b/backend/src/instrument.ts @@ -0,0 +1,6 @@ +/** + * Sentry instrumentation stub. + * The original initialized @sentry/nestjs here. + * Removed to eliminate centralized service dependency. + * Can be replaced with self-hosted Sentry or GlitchTip later. + */ diff --git a/backend/src/invites/entities/invite.entity.ts b/backend/src/invites/entities/invite.entity.ts new file mode 100644 index 0000000..c53bddb --- /dev/null +++ b/backend/src/invites/entities/invite.entity.ts @@ -0,0 +1,31 @@ +import { Content } from 'src/contents/entities/content.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; + +@Entity('invites') +export class Invite { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + contentId: string; + + @Column() + email: string; + + @Column({ default: 0 }) + share: number; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @ManyToOne(() => Content, (content) => content.invites) + @JoinColumn({ name: 'content_id' }) + content: Content; +} diff --git a/backend/src/invites/invites.module.ts b/backend/src/invites/invites.module.ts new file mode 100644 index 0000000..ff05659 --- /dev/null +++ b/backend/src/invites/invites.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Invite } from './entities/invite.entity'; +import { InvitesService } from './invites.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Invite])], + providers: [InvitesService], + exports: [InvitesService], +}) +export class InvitesModule {} diff --git a/backend/src/invites/invites.service.ts b/backend/src/invites/invites.service.ts new file mode 100644 index 0000000..05c2a5d --- /dev/null +++ b/backend/src/invites/invites.service.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Invite } from './entities/invite.entity'; +import { Repository } from 'typeorm'; +import { MailService } from 'src/mail/mail.service'; +import { randomUUID } from 'node:crypto'; + +@Injectable() +export class InvitesService { + constructor( + @InjectRepository(Invite) + private invitesRepository: Repository, + @Inject(MailService) + private mailService: MailService, + ) {} + + async getUserInvites(email: string) { + return this.invitesRepository.find({ + where: { email }, + }); + } + + async getFilmInvites(contentId: string, email?: string) { + return this.invitesRepository.find({ + where: { contentId, email }, + }); + } + + async sendInvite(email: string, contentId: string) { + const invite = await this.invitesRepository.save({ + id: randomUUID(), + email, + contentId, + }); + return invite; + } + + async sendInvites( + invites: Array<{ email: string; share: number }>, + contentId: string, + ) { + const parsedInvites = invites.map((invite) => ({ + id: randomUUID(), + email: invite.email, + contentId, + share: invite.share, + })); + await this.invitesRepository.save(parsedInvites); + return true; + } + + async removeInvite(email: string, contentId?: string) { + await this.invitesRepository.delete({ + email, + contentId, + }); + return true; + } + + async updateInvite(email: string, contentId: string, share: number) { + await this.invitesRepository.update({ email, contentId }, { share }); + return true; + } +} diff --git a/backend/src/library/dto/request/list-library.dto.ts b/backend/src/library/dto/request/list-library.dto.ts new file mode 100644 index 0000000..a7bfcaa --- /dev/null +++ b/backend/src/library/dto/request/list-library.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; + +export class ListLibraryDTO { + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; +} diff --git a/backend/src/library/entities/library-item.entity.ts b/backend/src/library/entities/library-item.entity.ts new file mode 100644 index 0000000..27d9e7f --- /dev/null +++ b/backend/src/library/entities/library-item.entity.ts @@ -0,0 +1,39 @@ +import { Project } from 'src/projects/entities/project.entity'; +import { User } from 'src/users/entities/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Unique, +} from 'typeorm'; + +@Entity('library_items') +@Unique('UQ_library_user_project', ['userId', 'projectId']) +export class LibraryItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + userId: string; + + @Column() + projectId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @ManyToOne(() => User, (user) => user.libraryItems, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Project, (project) => project.libraryItems, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'project_id' }) + project: Project; +} diff --git a/backend/src/library/library.controller.ts b/backend/src/library/library.controller.ts new file mode 100644 index 0000000..815a176 --- /dev/null +++ b/backend/src/library/library.controller.ts @@ -0,0 +1,63 @@ +import { + Controller, + Delete, + Get, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { BaseProjectDTO } from 'src/projects/dto/response/base-project.dto'; +import { ListLibraryDTO } from './dto/request/list-library.dto'; +import { LibraryService } from './library.service'; + +@Controller('library') +@UseGuards(HybridAuthGuard) +export class LibraryController { + constructor(private readonly libraryService: LibraryService) {} + + @Get() + async list( + @User() user: RequestUser['user'], + @Query() query: ListLibraryDTO, + ) { + const projects = await this.libraryService.listByUser(user.id, query); + return projects.map((project) => new BaseProjectDTO(project)); + } + + @Get('count') + async count(@User() user: RequestUser['user']) { + const count = await this.libraryService.countByUser(user.id); + return { count }; + } + + @Get(':projectId/exists') + async exists( + @User() user: RequestUser['user'], + @Param('projectId') projectId: string, + ) { + const exists = await this.libraryService.exists(user.id, projectId); + return { exists }; + } + + @Post(':projectId') + async add( + @User() user: RequestUser['user'], + @Param('projectId') projectId: string, + ) { + await this.libraryService.add(user.id, projectId); + return { added: true }; + } + + @Delete(':projectId') + async remove( + @User() user: RequestUser['user'], + @Param('projectId') projectId: string, + ) { + await this.libraryService.remove(user.id, projectId); + return { removed: true }; + } +} diff --git a/backend/src/library/library.module.ts b/backend/src/library/library.module.ts new file mode 100644 index 0000000..1444c4f --- /dev/null +++ b/backend/src/library/library.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LibraryController } from './library.controller'; +import { LibraryService } from './library.service'; +import { LibraryItem } from './entities/library-item.entity'; +import { Project } from 'src/projects/entities/project.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([LibraryItem, Project])], + controllers: [LibraryController], + providers: [LibraryService], +}) +export class LibraryModule {} diff --git a/backend/src/library/library.service.ts b/backend/src/library/library.service.ts new file mode 100644 index 0000000..2485ae0 --- /dev/null +++ b/backend/src/library/library.service.ts @@ -0,0 +1,87 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { LibraryItem } from './entities/library-item.entity'; +import { ListLibraryDTO } from './dto/request/list-library.dto'; +import { Project } from 'src/projects/entities/project.entity'; + +@Injectable() +export class LibraryService { + constructor( + @InjectRepository(LibraryItem) + private readonly libraryRepository: Repository, + @InjectRepository(Project) + private readonly projectRepository: Repository, + ) {} + + async listByUser(userId: string, query: ListLibraryDTO) { + const { limit = 30, offset = 0 } = query; + + const items = await this.libraryRepository.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + if (items.length === 0) return []; + + const ids = items.map((item) => item.projectId); + const projects = await this.projectRepository.find({ + where: { + id: In(ids), + status: 'published', + }, + relations: [ + 'contents', + 'genre', + 'projectSubgenres', + 'projectSubgenres.subgenre', + 'trailer', + ], + }); + + const projectById = new Map( + projects.map((project) => [project.id, project]), + ); + + return ids + .map((id) => projectById.get(id)) + .filter((project): project is Project => !!project); + } + + async countByUser(userId: string) { + return this.libraryRepository.count({ where: { userId } }); + } + + async exists(userId: string, projectId: string) { + return this.libraryRepository.exist({ + where: { userId, projectId }, + }); + } + + async add(userId: string, projectId: string) { + const project = await this.projectRepository.findOne({ + where: { id: projectId, status: 'published' }, + }); + + if (!project) { + throw new NotFoundException('Project not found'); + } + + const alreadyExists = await this.libraryRepository.exist({ + where: { userId, projectId }, + }); + + if (alreadyExists) { + return; + } + + const item = this.libraryRepository.create({ userId, projectId }); + await this.libraryRepository.save(item); + } + + async remove(userId: string, projectId: string) { + await this.libraryRepository.delete({ userId, projectId }); + } +} diff --git a/backend/src/mail/mail-data.ts b/backend/src/mail/mail-data.ts new file mode 100644 index 0000000..735f321 --- /dev/null +++ b/backend/src/mail/mail-data.ts @@ -0,0 +1,13 @@ +export default interface MailData { + to: string; + data?: any; + templateId: string; + attachments?: MailAttachment[]; +} + +export interface MailAttachment { + content: string; + filename: string; + type: string; + disposition: string; +} diff --git a/backend/src/mail/mail.module.ts b/backend/src/mail/mail.module.ts new file mode 100644 index 0000000..ad07b55 --- /dev/null +++ b/backend/src/mail/mail.module.ts @@ -0,0 +1,15 @@ +import { Global, Module } from '@nestjs/common'; +import { MailService } from './mail.service'; + +/** + * Mail module. + * Replaced SendGrid with nodemailer (SMTP). + * In development, use Mailpit for local SMTP testing. + * In production, configure a real SMTP server. + */ +@Global() +@Module({ + providers: [MailService], + exports: [MailService], +}) +export class MailModule {} diff --git a/backend/src/mail/mail.service.ts b/backend/src/mail/mail.service.ts new file mode 100644 index 0000000..d2900e1 --- /dev/null +++ b/backend/src/mail/mail.service.ts @@ -0,0 +1,71 @@ +import { Injectable, Logger } from '@nestjs/common'; +import * as nodemailer from 'nodemailer'; +import MailData from './mail-data'; + +/** + * Mail service using nodemailer (SMTP). + * Replaces SendGrid with a generic SMTP transport. + * In development, use Mailpit (localhost:1025) for testing. + */ +@Injectable() +export class MailService { + private transporter: nodemailer.Transporter; + private readonly logger = new Logger(MailService.name); + + constructor() { + const host = process.env.SMTP_HOST || 'localhost'; + const port = Number(process.env.SMTP_PORT || '1025'); + const user = process.env.SMTP_USER || ''; + const pass = process.env.SMTP_PASS || ''; + + this.transporter = nodemailer.createTransport({ + host, + port, + secure: false, + ...(user && pass ? { auth: { user, pass } } : {}), + }); + + this.logger.log(`Mail transport configured: ${host}:${port}`); + } + + async sendMail({ templateId, data = {}, to }: MailData) { + const from = process.env.MAIL_FROM || 'noreply@indeedhub.local'; + const subject = `IndeeHub notification (template: ${templateId})`; + + // Simple HTML email -- template engine can be added later + const html = ` +

IndeeHub Notification

+

Template: ${templateId}

+
${JSON.stringify(data, null, 2)}
+ `; + + try { + const info = await this.transporter.sendMail({ + from, + to, + subject, + html, + }); + this.logger.log(`Email sent: ${info.messageId}`); + } catch (error) { + this.logger.error(`Failed to send email: ${error.message}`); + } + } + + async addToList(_emails: string[], _listsId: string[]) { + this.logger.log('addToList called (no-op in SMTP mode)'); + } + + async sendBatch( + templateId: string, + recipients: { email: string; data?: Record }[], + ) { + for (const recipient of recipients) { + await this.sendMail({ + templateId, + to: recipient.email, + data: recipient.data || {}, + }); + } + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts new file mode 100644 index 0000000..7b171bf --- /dev/null +++ b/backend/src/main.ts @@ -0,0 +1,82 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common'; +import { useContainer } from 'class-validator'; +// Sentry instrumentation removed (see instrument.ts) +import * as express from 'express'; +import { + ExpressAdapter, + NestExpressApplication, +} from '@nestjs/platform-express'; +import { RawBodyRequest } from './types/raw-body-request'; + +async function bootstrap() { + const server = express(); + const captureRawBody = ( + request: RawBodyRequest, + _response: express.Response, + buffer: Buffer, + ) => { + request.rawBody = buffer.toString('utf8'); + }; + + server.use( + express.json({ + limit: '10mb', + verify: captureRawBody, + }), + ); + + server.use( + express.urlencoded({ + extended: true, + limit: '10mb', + verify: captureRawBody, + }), + ); + + const app = await NestFactory.create( + AppModule, + new ExpressAdapter(server), + { + bodyParser: false, + }, + ); + + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + + if (process.env.ENVIRONMENT === 'development') { + const swagConfig = new DocumentBuilder() + .setTitle('IndeeHub API') + .setDescription('This is the API for the IndeeHub application') + .setVersion('1.0') + .build(); + + const document = SwaggerModule.createDocument(app, swagConfig); + + SwaggerModule.setup('api', app, document); + } + + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + + if (process.env.ENVIRONMENT === 'production') { + app.enableCors({ + origin: [ + 'https://indeehub.studio', + 'https://www.indeehub.studio', + 'https://app.indeehub.studio', + 'https://bff.indeehub.studio', + 'https://indeehub.retool.com', + 'https://www.indeehub.retool.com', + ], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + credentials: true, + }); + } else app.enableCors(); + + await app.listen(process.env.PORT || 4000); +} + +// eslint-disable-next-line unicorn/prefer-top-level-await +bootstrap().catch(console.error); diff --git a/backend/src/migration/migration.controller.ts b/backend/src/migration/migration.controller.ts new file mode 100644 index 0000000..7e92fd7 --- /dev/null +++ b/backend/src/migration/migration.controller.ts @@ -0,0 +1,49 @@ +import { BadRequestException, Body, Controller, Post } from '@nestjs/common'; +import { MigrationService } from './migration.service'; + +@Controller('migration') +export class MigrationController { + constructor(private readonly migrationService: MigrationService) {} + + @Post('transcode') + async transcodeFiles(@Body('contentIds') contentIds: number[]) { + try { + await this.migrationService.transcodeFiles(contentIds); + return { message: 'Transcoding started' }; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Post('transcode-trailers') + async queueAllTrailers() { + try { + const result = await this.migrationService.queueAllTrailers(); + return { message: 'Trailer transcoding queued', ...result }; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Post('create-trailers-from-legacy') + async createTrailersFromLegacy() { + try { + const result = await this.migrationService.createTrailersFromLegacy(); + return { message: 'Legacy trailers migrated', ...result }; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Post('copy-legacy-trailers') + async copyLegacyTrailers(@Body('limit') limit?: number) { + try { + const normalizedLimit = + typeof limit === 'number' && limit > 0 ? limit : undefined; + this.migrationService.copyLegacyTrailersToPrivate(normalizedLimit); + return { message: 'Legacy trailers started copying to private bucket' }; + } catch (error) { + throw new BadRequestException(error.message); + } + } +} diff --git a/backend/src/migration/migration.module.ts b/backend/src/migration/migration.module.ts new file mode 100644 index 0000000..a7c9ecb --- /dev/null +++ b/backend/src/migration/migration.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { MigrationService } from './migration.service'; +import { MigrationController } from './migration.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Content } from 'src/contents/entities/content.entity'; +import { ContentsModule } from 'src/contents/contents.module'; +import { Subscription } from 'src/subscriptions/entities/subscription.entity'; +import { ConfigService } from '@nestjs/config'; +import { Project } from 'src/projects/entities/project.entity'; +import { ProjectsModule } from 'src/projects/projects.module'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Content, Project, Subscription, Trailer]), + ContentsModule, + ProjectsModule, + ], + controllers: [MigrationController], + providers: [MigrationService, ConfigService], +}) +export class MigrationModule {} diff --git a/backend/src/migration/migration.service.ts b/backend/src/migration/migration.service.ts new file mode 100644 index 0000000..95c069a --- /dev/null +++ b/backend/src/migration/migration.service.ts @@ -0,0 +1,285 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ContentsService } from 'src/contents/contents.service'; +import { Content } from 'src/contents/entities/content.entity'; + +import { In, Repository } from 'typeorm'; +import { Project } from 'src/projects/entities/project.entity'; +import { ProjectsService } from 'src/projects/projects.service'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; +import { UploadService } from 'src/upload/upload.service'; + +@Injectable() +export class MigrationService { + constructor( + @InjectRepository(Content) + private readonly contentRepository: Repository, + @InjectRepository(Project) + private readonly projectsRepository: Repository, + @InjectRepository(Trailer) + private readonly trailerRepository: Repository, + @Inject(ContentsService) + private readonly contentsService: ContentsService, + @Inject(ProjectsService) + private readonly projectsService: ProjectsService, + @Inject(UploadService) + private readonly uploadService: UploadService, + ) {} + async transcodeFiles(contentIds: number[]) { + const contents = await this.contentRepository.find({ + where: { id: In(contentIds) }, + select: ['id', 'file'], + take: 9, + relations: ['project'], + }); + + Logger.log(`Transcoding ${contents.length} files`); + for (const content of contents) { + await this.contentsService.sendToTranscodingQueue( + content, + content.project, + ); + } + } + + async queueAllTrailers() { + // Queue content trailers + const contents = await this.contentRepository.find({ + relations: ['trailer', 'project'], + select: { + id: true, + trailer: { + id: true, + file: true, + status: true, + }, + project: { + id: true, + status: true, + type: true, + }, + }, + take: 5000, + }); + + let contentQueued = 0; + for (const content of contents) { + if (content.trailer?.file && content.project?.status === 'published') { + await this.contentsService.sendTrailerToTranscodingQueue( + content, + content.project, + ); + contentQueued++; + } + } + + // Queue project trailers + const projects = await this.projectsRepository.find({ + where: {}, + relations: ['trailer'], + select: { + id: true, + status: true, + trailer: { + id: true, + file: true, + status: true, + }, + }, + take: 5000, + }); + + let projectQueued = 0; + for (const project of projects) { + if ( + project.trailer?.file && + project.status === 'published' && + project.trailer.status !== 'completed' + ) { + await this.projectsService.sendProjectTrailerToTranscodingQueue( + project, + ); + projectQueued++; + } + } + + Logger.log( + `Queued ${contentQueued} content trailers and ${projectQueued} project trailers for transcoding`, + 'MigrationService', + ); + return { contentQueued, projectQueued }; + } + + async createTrailersFromLegacy() { + // Migrate content.trailer_old -> trailers + contents.trailer_id + const contentRows: Array<{ id: string; trailer_old: string }> = + await this.contentRepository.query( + 'SELECT id, trailer_old FROM contents WHERE trailer_old IS NOT NULL AND trailer_id IS NULL', + ); + let contentCreated = 0; + for (const row of contentRows) { + if (!row.trailer_old) continue; + const trailer = await this.trailerRepository.save( + this.trailerRepository.create({ + file: row.trailer_old, + status: 'processing', + metadata: undefined, + }), + ); + await this.contentRepository.query( + 'UPDATE contents SET trailer_id = $1 WHERE id = $2', + [trailer.id, row.id], + ); + contentCreated++; + } + + // Migrate project.trailer_old -> trailers + projects.trailer_id + const projectRows: Array<{ id: string; trailer_old: string }> = + await this.projectsRepository.query( + 'SELECT id, trailer_old FROM projects WHERE trailer_old IS NOT NULL AND trailer_id IS NULL', + ); + let projectCreated = 0; + for (const row of projectRows) { + if (!row.trailer_old) continue; + const trailer = await this.trailerRepository.save( + this.trailerRepository.create({ + file: row.trailer_old, + status: 'processing', + metadata: undefined, + }), + ); + await this.projectsRepository.query( + 'UPDATE projects SET trailer_id = $1 WHERE id = $2', + [trailer.id, row.id], + ); + projectCreated++; + } + + return { contentCreated, projectCreated }; + } + + async copyLegacyTrailersToPrivate(cappedLimit = 5000) { + const publicBucket = process.env.S3_PUBLIC_BUCKET_NAME; + const privateBucket = process.env.S3_PRIVATE_BUCKET_NAME; + + if (!publicBucket || !privateBucket) { + throw new Error('S3 buckets are not configured.'); + } + + const contentRows: Array<{ id: string; trailer_old: string }> = + await this.contentRepository.query( + 'SELECT c.id, c.trailer_old FROM contents c INNER JOIN trailers t ON c.trailer_id = t.id WHERE c.trailer_old IS NOT NULL AND t.file = c.trailer_old LIMIT $1', + [cappedLimit], + ); + const projectRows: Array<{ id: string; trailer_old: string }> = + await this.projectsRepository.query( + 'SELECT p.id, p.trailer_old FROM projects p INNER JOIN trailers t ON p.trailer_id = t.id WHERE p.trailer_old IS NOT NULL AND t.file = p.trailer_old LIMIT $1', + [cappedLimit], + ); + + const results = { + contents: { + total: contentRows.length, + copied: 0, + missing: 0, + skipped: 0, + failed: 0, + }, + projects: { + total: projectRows.length, + copied: 0, + missing: 0, + skipped: 0, + failed: 0, + }, + failedItems: [] as Array<{ + type: 'content' | 'project'; + id: string; + key: string; + error: string; + }>, + }; + + const recordFailure = ( + type: 'content' | 'project', + id: string, + key: string, + error: unknown, + ) => { + if (type === 'content') { + results.contents.failed++; + } else { + results.projects.failed++; + } + if (results.failedItems.length < 25) { + results.failedItems.push({ + type, + id, + key, + error: error instanceof Error ? error.message : String(error), + }); + } + }; + + const copyIfNeeded = async ( + type: 'content' | 'project', + id: string, + key: string, + ) => { + if (!key) return; + + try { + const existsInPublic = await this.uploadService.objectExists( + key, + publicBucket, + ); + if (!existsInPublic) { + if (type === 'content') { + results.contents.missing++; + } else { + results.projects.missing++; + } + return; + } + + const existsInPrivate = await this.uploadService.objectExists( + key, + privateBucket, + ); + if (existsInPrivate) { + if (type === 'content') { + results.contents.skipped++; + } else { + results.projects.skipped++; + } + return; + } + + await this.uploadService.copyObject({ + sourceBucket: publicBucket, + destinationBucket: privateBucket, + key, + }); + + if (type === 'content') { + results.contents.copied++; + } else { + results.projects.copied++; + } + } catch (error) { + Logger.error(`Failed to copy trailer key: ${key}`, error); + recordFailure(type, id, key, error); + } + }; + + for (const row of contentRows) { + await copyIfNeeded('content', row.id, row.trailer_old); + } + + for (const row of projectRows) { + await copyIfNeeded('project', row.id, row.trailer_old); + } + + Logger.log({ results }, 'Migration'); + } +} diff --git a/backend/src/nostr-auth/__tests__/nostr-auth.guard.spec.ts b/backend/src/nostr-auth/__tests__/nostr-auth.guard.spec.ts new file mode 100644 index 0000000..90cb96d --- /dev/null +++ b/backend/src/nostr-auth/__tests__/nostr-auth.guard.spec.ts @@ -0,0 +1,191 @@ +import { UnauthorizedException, type ExecutionContext } from '@nestjs/common'; +import { createHash } from 'node:crypto'; +import { + finalizeEvent, + generateSecretKey, + getPublicKey, + type UnsignedEvent, +} from 'nostr-tools'; +import type { Request } from 'express'; +import { NostrAuthGuard } from '../nostr-auth.guard'; +import { NostrAuthService } from '../nostr-auth.service'; +import { NostrAuthErrorCode } from '../nostr-auth.errors'; +import { RawBodyRequest } from '../../types/raw-body-request'; + +const hashPayload = (payload: string) => + createHash('sha256').update(payload).digest('hex'); + +const host = 'nostr.test'; +const path = '/nostr-auth/echo'; +const url = `http://${host}${path}`; + +const secretKey = generateSecretKey(); +const pubkey = getPublicKey(secretKey); + +const buildAuthHeader = (unsignedEvent: UnsignedEvent): string => { + const event = finalizeEvent(unsignedEvent, secretKey); + return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`; +}; + +const createExecutionContext = (request: any): ExecutionContext => + ({ + switchToHttp: () => ({ + getRequest: () => request, + }), + }) as ExecutionContext; + +describe('NostrAuthGuard', () => { + const service = new NostrAuthService(); + const guard = new NostrAuthGuard(service); + + const baseBody = { ping: 'pong' }; + const basePayload = JSON.stringify(baseBody); + + const baseUnsignedEvent: UnsignedEvent = { + pubkey, + kind: 27_235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(basePayload)], + ], + content: '', + }; + + const buildRequest = ( + authorization: string, + body: unknown = baseBody, + options?: { rawBody?: string | null }, + ): RawBodyRequest => { + const computedRawBody = + options?.rawBody === null + ? undefined + : (options?.rawBody ?? + (typeof body === 'string' ? (body as string) : JSON.stringify(body))); + + return { + method: 'POST', + protocol: 'http', + headers: { + authorization, + host, + 'x-forwarded-proto': 'http', + }, + originalUrl: path, + body, + rawBody: computedRawBody, + get: function get(header: string) { + return this.headers[header.toLowerCase()]; + }, + } as unknown as RawBodyRequest; + }; + + it('attaches pubkey and event on success', async () => { + const authorization = buildAuthHeader(baseUnsignedEvent); + const request = buildRequest(authorization); + const context = createExecutionContext(request); + + const result = await guard.canActivate(context); + + expect(result).toBe(true); + expect(request.nostrPubkey).toBe(pubkey); + expect(request.nostrEvent).toBeDefined(); + }); + + it('maps payload mismatch to unauthorized', async () => { + const badPayload = JSON.stringify({ tampered: true }); + const authorization = buildAuthHeader({ + ...baseUnsignedEvent, + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(badPayload)], + ], + }); + + const request = buildRequest(authorization, baseBody); + const context = createExecutionContext(request); + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException, + ); + + try { + await guard.canActivate(context); + } catch (error) { + const response = (error as UnauthorizedException).getResponse() as { + code?: string; + }; + expect(response.code).toBe(NostrAuthErrorCode.PAYLOAD_MISMATCH); + } + }); + + it('uses nostr-authorization header when bearer token is present', async () => { + const authorization = buildAuthHeader(baseUnsignedEvent); + const request = buildRequest('Bearer jwt-token'); + request.headers['nostr-authorization'] = authorization; + const context = createExecutionContext(request); + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(request.nostrPubkey).toBe(pubkey); + }); + + it('derives payload from string body when rawBody is absent', async () => { + const body = 'raw-payload'; + const authorization = buildAuthHeader({ + ...baseUnsignedEvent, + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(body)], + ], + }); + + const request = buildRequest(authorization, body, { rawBody: undefined }); + const context = createExecutionContext(request); + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(request.nostrPubkey).toBe(pubkey); + }); + + it('derives payload from parsed object body when rawBody is absent', async () => { + const body = { foo: 'bar' }; + const payload = JSON.stringify(body); + const authorization = buildAuthHeader({ + ...baseUnsignedEvent, + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(payload)], + ], + }); + + const request = buildRequest(authorization, body, { rawBody: undefined }); + const context = createExecutionContext(request); + + const result = await guard.canActivate(context); + expect(result).toBe(true); + expect(request.nostrPubkey).toBe(pubkey); + }); + + it('wraps unexpected errors as unauthorized', async () => { + const authorization = buildAuthHeader(baseUnsignedEvent); + const request = buildRequest(authorization); + const context = createExecutionContext(request); + + const spy = jest + .spyOn(service, 'verifyAuthorizationHeader') + .mockImplementation(() => { + throw new Error('boom'); + }); + + await expect(guard.canActivate(context)).rejects.toThrow( + UnauthorizedException, + ); + + spy.mockRestore(); + }); +}); diff --git a/backend/src/nostr-auth/__tests__/nostr-auth.service.spec.ts b/backend/src/nostr-auth/__tests__/nostr-auth.service.spec.ts new file mode 100644 index 0000000..0d66c1f --- /dev/null +++ b/backend/src/nostr-auth/__tests__/nostr-auth.service.spec.ts @@ -0,0 +1,216 @@ +import { createHash } from 'node:crypto'; +import { + finalizeEvent, + generateSecretKey, + getPublicKey, + type Event, + type UnsignedEvent, +} from 'nostr-tools'; +import { NostrAuthError, NostrAuthErrorCode } from '../nostr-auth.errors'; +import { NostrAuthService } from '../nostr-auth.service'; + +const hashPayload = (payload: string) => + createHash('sha256').update(payload).digest('hex'); + +const expectErrorCode = ( + function_: () => void, + code: NostrAuthErrorCode, +): void => { + try { + function_(); + throw new Error('Expected error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(NostrAuthError); + expect((error as NostrAuthError).code).toBe(code); + } +}; + +describe('NostrAuthService', () => { + const service = new NostrAuthService(); + const secretKey = generateSecretKey(); + const pubkey = getPublicKey(secretKey); + const defaultMethod = 'POST'; + const defaultUrl = 'http://nostr.test/nostr-auth/echo'; + const defaultPayload = JSON.stringify({ hello: 'world' }); + + const buildUnsignedEvent = ( + overrides: Partial & { + payloadOverride?: string; + urlOverride?: string; + methodOverride?: string; + } = {}, + ): UnsignedEvent => { + const payload = overrides.payloadOverride ?? defaultPayload; + const url = overrides.urlOverride ?? defaultUrl; + const method = overrides.methodOverride ?? defaultMethod; + + return { + pubkey, + kind: 27_235, + created_at: overrides.created_at ?? Math.floor(Date.now() / 1000), + tags: overrides.tags ?? [ + ['u', url], + ['method', method], + ['payload', hashPayload(payload)], + ], + content: '', + }; + }; + + const buildAuthHeader = ( + unsignedEvent?: UnsignedEvent, + mutate?: (event: Event) => Event, + ): string => { + const finalized = finalizeEvent( + unsignedEvent ?? buildUnsignedEvent(), + secretKey, + ); + const event = mutate ? mutate(finalized) : finalized; + return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`; + }; + + it('verifies a valid authorization header', () => { + const authorization = buildAuthHeader(); + + const result = service.verifyAuthorizationHeader({ + authorization, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }); + + expect(result.pubkey).toBe(pubkey); + expect(result.event.pubkey).toBe(pubkey); + }); + + it('throws when header is missing', () => { + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization: undefined, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.MISSING_AUTH_HEADER, + ); + }); + + it('throws on bad scheme', () => { + const authorization = buildAuthHeader().replace(/^Nostr/, 'Bearer'); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.BAD_SCHEME, + ); + }); + + it('throws on invalid event payload', () => { + const badPayload = Buffer.from(JSON.stringify({ foo: 'bar' })).toString( + 'base64', + ); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization: `Nostr ${badPayload}`, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.INVALID_EVENT, + ); + }); + + it('throws on invalid signature', () => { + const authorization = buildAuthHeader(undefined, (event) => ({ + ...event, + sig: '00'.repeat(64), + })); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.INVALID_SIGNATURE, + ); + }); + + it('throws on stale event', () => { + const authorization = buildAuthHeader( + buildUnsignedEvent({ created_at: Math.floor(Date.now() / 1000) - 1000 }), + ); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.STALE_EVENT, + ); + }); + + it('throws on method mismatch', () => { + const authorization = buildAuthHeader(); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization, + method: 'GET', + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.METHOD_MISMATCH, + ); + }); + + it('throws on url mismatch', () => { + const authorization = buildAuthHeader( + buildUnsignedEvent({ urlOverride: 'http://nostr.test/other' }), + ); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.URL_MISMATCH, + ); + }); + + it('throws on payload mismatch', () => { + const authorization = buildAuthHeader( + buildUnsignedEvent({ + payloadOverride: JSON.stringify({ tampered: true }), + }), + ); + + expectErrorCode( + () => + service.verifyAuthorizationHeader({ + authorization, + method: defaultMethod, + url: defaultUrl, + payload: defaultPayload, + }), + NostrAuthErrorCode.PAYLOAD_MISMATCH, + ); + }); +}); diff --git a/backend/src/nostr-auth/nostr-auth.controller.ts b/backend/src/nostr-auth/nostr-auth.controller.ts new file mode 100644 index 0000000..b0d3558 --- /dev/null +++ b/backend/src/nostr-auth/nostr-auth.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common'; +import { Request } from 'express'; +import { NostrAuthGuard } from './nostr-auth.guard'; + +@Controller('nostr-auth') +export class NostrAuthController { + @UseGuards(NostrAuthGuard) + @Post('echo') + echo(@Req() request: Request) { + return { + pubkey: request.nostrPubkey, + event: request.nostrEvent, + }; + } + + @Get('health') + health() { + return { status: 'ok' }; + } +} diff --git a/backend/src/nostr-auth/nostr-auth.errors.ts b/backend/src/nostr-auth/nostr-auth.errors.ts new file mode 100644 index 0000000..bce48bb --- /dev/null +++ b/backend/src/nostr-auth/nostr-auth.errors.ts @@ -0,0 +1,20 @@ +export enum NostrAuthErrorCode { + MISSING_AUTH_HEADER = 'MISSING_AUTH_HEADER', + BAD_SCHEME = 'BAD_SCHEME', + INVALID_EVENT = 'INVALID_EVENT', + INVALID_SIGNATURE = 'INVALID_SIGNATURE', + STALE_EVENT = 'STALE_EVENT', + METHOD_MISMATCH = 'METHOD_MISMATCH', + URL_MISMATCH = 'URL_MISMATCH', + PAYLOAD_MISMATCH = 'PAYLOAD_MISMATCH', +} + +export class NostrAuthError extends Error { + constructor( + public readonly code: NostrAuthErrorCode, + message?: string, + ) { + super(message ?? code); + this.name = 'NostrAuthError'; + } +} diff --git a/backend/src/nostr-auth/nostr-auth.guard.ts b/backend/src/nostr-auth/nostr-auth.guard.ts new file mode 100644 index 0000000..a11ceeb --- /dev/null +++ b/backend/src/nostr-auth/nostr-auth.guard.ts @@ -0,0 +1,115 @@ +import { + BadRequestException, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Request } from 'express'; +import { NostrAuthError, NostrAuthErrorCode } from './nostr-auth.errors'; +import { NostrAuthService } from './nostr-auth.service'; +import { RawBodyRequest } from '../types/raw-body-request'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class NostrAuthGuard extends AuthGuard('nostr') { + constructor(private readonly nostrAuthService: NostrAuthService) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context + .switchToHttp() + .getRequest>(); + + try { + const { pubkey, event } = this.nostrAuthService.verifyAuthorizationHeader( + { + authorization: this.getAuthorizationHeader(request), + method: request.method, + url: this.buildRequestUrl(request), + payload: this.getPayload(request), + }, + ); + + request.nostrPubkey = pubkey; + request.nostrEvent = event; + return true; + } catch (error) { + if (error instanceof NostrAuthError) { + throw this.mapErrorToHttpException(error); + } + + throw new UnauthorizedException({ + code: 'UNKNOWN_ERROR', + message: 'Unable to authenticate request', + }); + } + } + + private getAuthorizationHeader(request: Request): string | undefined { + const nostrHeader = + request.headers['nostr-authorization'] ?? + request.headers['x-nostr-authorization']; + + if (nostrHeader) { + return Array.isArray(nostrHeader) ? nostrHeader[0] : nostrHeader; + } + + const header = request.headers.authorization; + const value = Array.isArray(header) ? (header.find(Boolean) ?? '') : header; + + if (value?.toLowerCase().startsWith('nostr ')) { + return value; + } + + return undefined; + } + + private getPayload(request: RawBodyRequest): string { + if (typeof request.rawBody === 'string') { + return request.rawBody; + } + + if (typeof request.body === 'string') { + return request.body; + } + + if (request.body && Object.keys(request.body).length > 0) { + return JSON.stringify(request.body); + } + + return ''; + } + + private buildRequestUrl(request: Request): string { + const protocolHeader = + (request.headers['x-forwarded-proto'] as string | undefined) ?? + request.protocol ?? + 'http'; + const host = + request.get('host') ?? request.headers.host ?? request.hostname; + const path = request.originalUrl ?? request.url ?? ''; + + return `${protocolHeader}://${host}${path}`; + } + + private mapErrorToHttpException(error: NostrAuthError) { + const badRequestCodes = new Set([ + NostrAuthErrorCode.INVALID_EVENT, + NostrAuthErrorCode.METHOD_MISMATCH, + NostrAuthErrorCode.URL_MISMATCH, + ]); + + if (badRequestCodes.has(error.code)) { + return new BadRequestException({ + code: error.code, + message: error.message, + }); + } + + return new UnauthorizedException({ + code: error.code, + message: error.message, + }); + } +} diff --git a/backend/src/nostr-auth/nostr-auth.module.ts b/backend/src/nostr-auth/nostr-auth.module.ts new file mode 100644 index 0000000..65ec318 --- /dev/null +++ b/backend/src/nostr-auth/nostr-auth.module.ts @@ -0,0 +1,14 @@ +import { Global, Module } from '@nestjs/common'; +import { NostrAuthGuard } from './nostr-auth.guard'; +import { NostrAuthService } from './nostr-auth.service'; +import { NostrAuthController } from './nostr-auth.controller'; + +@Global() +@Module({ + controllers: [NostrAuthController], + providers: [NostrAuthService, NostrAuthGuard], + exports: [NostrAuthService, NostrAuthGuard], +}) +export class NostrAuthModule {} + +export { NostrAuthError, NostrAuthErrorCode } from './nostr-auth.errors'; diff --git a/backend/src/nostr-auth/nostr-auth.service.ts b/backend/src/nostr-auth/nostr-auth.service.ts new file mode 100644 index 0000000..37caa95 --- /dev/null +++ b/backend/src/nostr-auth/nostr-auth.service.ts @@ -0,0 +1,182 @@ +import { Injectable } from '@nestjs/common'; +import { createHash } from 'node:crypto'; +import { getEventHash, validateEvent, verifyEvent } from 'nostr-tools'; +import type { Event } from 'nostr-tools'; +import { NostrAuthError, NostrAuthErrorCode } from './nostr-auth.errors'; + +export interface VerifyAuthorizationHeaderParameters { + authorization?: string; + method: string; + url: string; + payload?: string; +} + +export interface NostrAuthVerificationResult { + pubkey: string; + event: Event; +} + +@Injectable() +export class NostrAuthService { + verifyAuthorizationHeader( + parameters: VerifyAuthorizationHeaderParameters, + ): NostrAuthVerificationResult { + const { authorization, method, url, payload } = parameters; + + if (!authorization) { + throw new NostrAuthError( + NostrAuthErrorCode.MISSING_AUTH_HEADER, + 'Authorization header is missing', + ); + } + + const [scheme, encodedEvent] = authorization.split(' '); + + if (!encodedEvent || scheme?.toLowerCase() !== 'nostr') { + throw new NostrAuthError( + NostrAuthErrorCode.BAD_SCHEME, + 'Authorization scheme must be "Nostr "', + ); + } + + const event = this.decodeEvent(encodedEvent); + + this.validateEvent(event); + this.validateSignature(event); + this.assertFreshness(event); + this.assertMethod(event, method); + this.assertUrl(event, url); + this.assertPayload(event, payload ?? ''); + + return { + pubkey: event.pubkey, + event, + }; + } + + private decodeEvent(encodedEvent: string): Event { + try { + const jsonEvent = Buffer.from(encodedEvent, 'base64').toString('utf8'); + const event = JSON.parse(jsonEvent) as Event; + return event; + } catch { + throw new NostrAuthError( + NostrAuthErrorCode.INVALID_EVENT, + 'Authorization payload is not valid base64 JSON', + ); + } + } + + private validateEvent(event: Event) { + const isValidShape = validateEvent(event); + if (!isValidShape || event.kind !== 27_235) { + throw new NostrAuthError( + NostrAuthErrorCode.INVALID_EVENT, + 'Event is not a valid NIP-98 HTTP auth event', + ); + } + } + + private validateSignature(event: Event) { + const eventHash = getEventHash(event); + if (event.id && event.id !== eventHash) { + throw new NostrAuthError( + NostrAuthErrorCode.INVALID_SIGNATURE, + 'Event hash does not match id', + ); + } + + if (!verifyEvent(event)) { + throw new NostrAuthError( + NostrAuthErrorCode.INVALID_SIGNATURE, + 'Invalid nostr signature', + ); + } + } + + private assertFreshness(event: Event) { + const now = Math.floor(Date.now() / 1000); + const delta = Math.abs(now - event.created_at); + + if (delta > 120) { + throw new NostrAuthError( + NostrAuthErrorCode.STALE_EVENT, + 'Event timestamp is outside the allowed window', + ); + } + } + + private assertMethod(event: Event, method: string) { + const methodTag = this.findTagValue(event, 'method'); + if (!methodTag) { + throw new NostrAuthError( + NostrAuthErrorCode.METHOD_MISMATCH, + 'Event missing method tag', + ); + } + + if (methodTag.toUpperCase() !== method.toUpperCase()) { + throw new NostrAuthError( + NostrAuthErrorCode.METHOD_MISMATCH, + 'HTTP method does not match event tag', + ); + } + } + + private assertUrl(event: Event, url: string) { + const eventUrl = this.findTagValue(event, 'u'); + if (!eventUrl) { + throw new NostrAuthError( + NostrAuthErrorCode.URL_MISMATCH, + 'Event missing URL tag', + ); + } + + if (this.normalizeUrl(eventUrl) !== this.normalizeUrl(url)) { + throw new NostrAuthError( + NostrAuthErrorCode.URL_MISMATCH, + 'Request URL does not match event tag', + ); + } + } + + private assertPayload(event: Event, payload: string) { + const payloadTag = this.findTagValue(event, 'payload'); + + // If payload exists but tag is missing or empty, treat as mismatch. + if (!payloadTag && payload) { + throw new NostrAuthError( + NostrAuthErrorCode.PAYLOAD_MISMATCH, + 'Payload provided but payload tag missing', + ); + } + + if (payloadTag) { + const computedPayloadHash = this.hashPayload(payload); + if (computedPayloadHash !== payloadTag) { + throw new NostrAuthError( + NostrAuthErrorCode.PAYLOAD_MISMATCH, + 'Payload hash does not match event tag', + ); + } + } + } + + private hashPayload(payload: string): string { + return createHash('sha256').update(payload).digest('hex'); + } + + private normalizeUrl(url: string): string { + try { + const parsedUrl = new URL(url); + parsedUrl.hash = ''; + return parsedUrl.toString(); + } catch { + return url; + } + } + + private findTagValue(event: Event, key: string): string | undefined { + return event.tags.find((tag) => tag[0] === key)?.[1]; + } +} diff --git a/backend/src/payment/dto/request/add-bank.dto.ts b/backend/src/payment/dto/request/add-bank.dto.ts new file mode 100644 index 0000000..bb472a2 --- /dev/null +++ b/backend/src/payment/dto/request/add-bank.dto.ts @@ -0,0 +1,108 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + ArrayMinSize, + IsArray, + IsEmail, + IsEnum, + IsNotEmptyObject, + IsOptional, + IsUrl, + Length, + ValidateNested, +} from 'class-validator'; + +export class AddressDTO { + @ApiProperty() + @Length(2) + country: string; + + @ApiProperty() + @IsOptional() + @Length(1, 150) + state?: string; + + @ApiProperty() + @Length(1, 150) + city: string; + + @ApiProperty() + @Length(1, 150) + postCode: string; + + @ApiProperty() + @Length(1, 150) + line1: string; +} + +export class BeneficiaryDTO { + @ApiProperty() + @IsOptional() + dateOfBirth: Date; + + @ApiProperty() + @IsEnum(['INDIVIDUAL', 'COMPANY']) + type: 'INDIVIDUAL' | 'COMPANY' = 'INDIVIDUAL'; + + @ApiProperty() + @Length(1, 100) + name: string; + + @ApiProperty() + @ValidateNested() + @Type(() => AddressDTO) + @IsNotEmptyObject() + address: AddressDTO; + + @ApiProperty() + @IsEmail() + @IsOptional() + email?: string; + + @ApiProperty() + @Length(1, 150) + @IsOptional() + phoneNumber?: string; + + @ApiProperty() + @IsUrl() + @IsOptional() + url?: string; +} + +export class AddBankDTO { + @ApiProperty() + @IsEnum(['ACH', 'US_DOMESTIC_WIRE']) + transferType: 'ACH' | 'US_DOMESTIC_WIRE'; + + @ApiProperty() + @Length(5, 17) + accountNumber: string; + + @ApiProperty() + @Length(9, 9) + routingNumber: string; + + @ApiProperty() + @IsEnum(['CHECKING', 'SAVINGS']) + @IsOptional() + accountType?: 'CHECKING' | 'SAVINGS'; + + @ApiProperty() + @IsOptional() + @Length(1, 150) + bankName?: string; + + @ApiProperty() + @ValidateNested() + @IsNotEmptyObject() + @Type(() => AddressDTO) + bankAddress: AddressDTO; + + @ApiProperty() + @ValidateNested() + @Type(() => BeneficiaryDTO) + @IsArray() + @ArrayMinSize(1) + beneficiaries: BeneficiaryDTO[]; +} diff --git a/backend/src/payment/dto/request/add-payment-method.dto.ts b/backend/src/payment/dto/request/add-payment-method.dto.ts new file mode 100644 index 0000000..7f89814 --- /dev/null +++ b/backend/src/payment/dto/request/add-payment-method.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsOptional, + IsString, + Length, + ValidateNested, +} from 'class-validator'; +import { AddBankDTO } from './add-bank.dto'; +import { IsValidAddress } from 'src/payment/validators/lightning-address.validator'; +import { frequencies, Frequency } from 'src/payment/enums/frequency.enum'; + +export default class AddPaymentMethodDTO { + @ApiProperty() + @IsOptional() + @ValidateNested() + @Type(() => AddBankDTO) + bank?: AddBankDTO; + + @ApiProperty() + @IsOptional() + @IsString() + @Length(5, 100) + @IsValidAddress({ + message: 'lightningAddress must be a valid lightning address', + }) + lightningAddress?: string; + + @IsOptional() + @IsEnum(frequencies) + withdrawalFrequency: Frequency; +} diff --git a/backend/src/payment/dto/request/send-bank-payout.dto.ts b/backend/src/payment/dto/request/send-bank-payout.dto.ts new file mode 100644 index 0000000..02949d1 --- /dev/null +++ b/backend/src/payment/dto/request/send-bank-payout.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +export class SendBankPayoutDTO { + @ApiProperty() + @IsNumber() + amount: number; + + @ApiProperty() + @IsOptional() + @IsString() + contentId?: string; +} diff --git a/backend/src/payment/dto/request/send-payment.dto.ts b/backend/src/payment/dto/request/send-payment.dto.ts new file mode 100644 index 0000000..b285543 --- /dev/null +++ b/backend/src/payment/dto/request/send-payment.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; + +export class SendPaymentDTO { + @ApiProperty() + @IsEnum(['ZBD', 'STRIKE']) + provider: 'ZBD' | 'STRIKE' = 'ZBD'; +} diff --git a/backend/src/payment/dto/request/update-payment-method.dto.ts b/backend/src/payment/dto/request/update-payment-method.dto.ts new file mode 100644 index 0000000..1ab773a --- /dev/null +++ b/backend/src/payment/dto/request/update-payment-method.dto.ts @@ -0,0 +1,11 @@ +import { PartialType } from '@nestjs/swagger'; +import AddPaymentMethodDTO from './add-payment-method.dto'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export default class UpdatePaymentMethodDTO extends PartialType( + AddPaymentMethodDTO, +) { + @IsBoolean() + @IsOptional() + selected: boolean = false; +} diff --git a/backend/src/payment/dto/response/lightning-payment.dto.ts b/backend/src/payment/dto/response/lightning-payment.dto.ts new file mode 100644 index 0000000..4625f5a --- /dev/null +++ b/backend/src/payment/dto/response/lightning-payment.dto.ts @@ -0,0 +1,4 @@ +export class LightningPaymentDTO { + id: string; + status: string; +} diff --git a/backend/src/payment/dto/response/payment-method.dto.ts b/backend/src/payment/dto/response/payment-method.dto.ts new file mode 100644 index 0000000..7998f0f --- /dev/null +++ b/backend/src/payment/dto/response/payment-method.dto.ts @@ -0,0 +1,29 @@ +import { PaymentMethod } from 'src/payment/entities/payment-method.entity'; +import { PaymentMethod as StrikePaymentMethod } from '../../providers/dto/strike/payment-method'; +import { Frequency } from 'src/payment/enums/frequency.enum'; + +export class PaymentMethodDTO { + id: string; + type: 'BANK' | 'LIGHTNING'; + bankInfo?: StrikePaymentMethod; + lightningAddress?: string; + selected?: boolean; + withdrawalFrequency?: Frequency; + + constructor( + paymentMethod: PaymentMethod & { bankInfo?: StrikePaymentMethod }, + ) { + this.id = paymentMethod.id; + this.type = paymentMethod.type; + if (paymentMethod.bankInfo) + this.bankInfo = { + ...paymentMethod.bankInfo, + accountNumber: paymentMethod.bankInfo?.accountNumber?.slice(-4), + routingNumber: paymentMethod.bankInfo?.routingNumber?.slice(-4), + beneficiaries: undefined, + }; + this.lightningAddress = paymentMethod.lightningAddress; + this.selected = paymentMethod.selected; + this.withdrawalFrequency = paymentMethod.withdrawalFrequency; + } +} diff --git a/backend/src/payment/dto/response/payment.dto.ts b/backend/src/payment/dto/response/payment.dto.ts new file mode 100644 index 0000000..193ca97 --- /dev/null +++ b/backend/src/payment/dto/response/payment.dto.ts @@ -0,0 +1,36 @@ +import { Payment } from 'src/payment/entities/payment.entity'; +import { StrikePayout } from '../../providers/dto/strike/payout'; +import { PaymentExecuted } from '../../providers/dto/strike/payment-executed'; +import { PaymentMethodDTO } from './payment-method.dto'; +import { Type } from 'src/payment/enums/type.enum'; + +export class PaymentDTO { + id: string; + lightningInfo?: PaymentExecuted; + bankInfo?: StrikePayout; + milisatsAmount: number; + usdAmount: number; + comment: string; + invoice: string; + status: string; + type: Type; + createdAt: Date; + paymentMethod: PaymentMethodDTO; + constructor( + payment: Payment & { + lightningInfo?: PaymentExecuted; + bankInfo?: StrikePayout; + }, + ) { + this.id = payment.id; + this.lightningInfo = payment.lightningInfo; + this.bankInfo = payment.bankInfo; + this.milisatsAmount = payment.milisatsAmount; + this.usdAmount = payment.usdAmount; + this.status = payment.status; + this.createdAt = payment.createdAt; + this.type = payment.type; + if (payment.paymentMethod) + this.paymentMethod = new PaymentMethodDTO(payment.paymentMethod); + } +} diff --git a/backend/src/payment/entities/payment-method.entity.ts b/backend/src/payment/entities/payment-method.entity.ts new file mode 100644 index 0000000..058a79b --- /dev/null +++ b/backend/src/payment/entities/payment-method.entity.ts @@ -0,0 +1,51 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryColumn, +} from 'typeorm'; +import { Payment } from './payment.entity'; +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { Frequency } from '../enums/frequency.enum'; + +@Entity('payment_methods') +export class PaymentMethod { + @PrimaryColumn() + id: string; + + @Column() + filmmakerId: string; + + @Column({ nullable: true }) + lightningAddress: string; + + @Column({ nullable: true }) + providerId: string; + + @Column({ default: 'LIGHTNING' }) + type: 'BANK' | 'LIGHTNING'; + + @Column({ default: false }) + selected: boolean; + + @Column({ default: 'automatic' }) + withdrawalFrequency: Frequency; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @ManyToOne(() => Filmmaker, (filmmaker) => filmmaker.paymentMethods) + @JoinColumn({ name: 'filmmaker_id', referencedColumnName: 'id' }) + filmmaker: Filmmaker; + + @OneToMany(() => Payment, (payment) => payment.paymentMethod) + @JoinColumn({ name: 'payment_id', referencedColumnName: 'id' }) + payments: Payment[]; +} diff --git a/backend/src/payment/entities/payment.entity.ts b/backend/src/payment/entities/payment.entity.ts new file mode 100644 index 0000000..98f1774 --- /dev/null +++ b/backend/src/payment/entities/payment.entity.ts @@ -0,0 +1,50 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; +import { Status } from '../enums/status.enum'; +import { PaymentMethod } from './payment-method.entity'; +import { Type } from '../enums/type.enum'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; + +@Entity('payments') +export class Payment { + @PrimaryColumn() + id: string; + + @Column() + shareholderId: string; + + @Column() + providerId: string; + + @Column({ nullable: true }) + paymentMethodId: string; + + @Column({ nullable: true }) + milisatsAmount: number; + + @Column('decimal', { precision: 10, scale: 4 }) + usdAmount: number; + + @Column({ default: 'watch' }) + type: Type; + + @Column({ default: 'pending' }) + status: Status; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @ManyToOne(() => Shareholder, (shareholder) => shareholder.payments) + @JoinColumn({ name: 'shareholder_id', referencedColumnName: 'id' }) + shareholder: Shareholder; + + @ManyToOne(() => PaymentMethod, (paymentMethod) => paymentMethod.payments) + @JoinColumn({ name: 'payment_method_id', referencedColumnName: 'id' }) + paymentMethod: PaymentMethod; +} diff --git a/backend/src/payment/enums/filter-date-range.enum.ts b/backend/src/payment/enums/filter-date-range.enum.ts new file mode 100644 index 0000000..2be8191 --- /dev/null +++ b/backend/src/payment/enums/filter-date-range.enum.ts @@ -0,0 +1,8 @@ +export const filterDateRange = [ + 'last_7_days', + 'last_28_days', + 'last_60_days', + 'last_365_days', + 'since_uploaded', +] as const; +export type FilterDateRange = (typeof filterDateRange)[number]; diff --git a/backend/src/payment/enums/frequency.enum.ts b/backend/src/payment/enums/frequency.enum.ts new file mode 100644 index 0000000..265ae4e --- /dev/null +++ b/backend/src/payment/enums/frequency.enum.ts @@ -0,0 +1,2 @@ +export const frequencies = ['automatic', 'daily', 'weekly', 'monthly'] as const; +export type Frequency = (typeof frequencies)[number]; diff --git a/backend/src/payment/enums/status.enum.ts b/backend/src/payment/enums/status.enum.ts new file mode 100644 index 0000000..555a4d0 --- /dev/null +++ b/backend/src/payment/enums/status.enum.ts @@ -0,0 +1,2 @@ +export const statuses = ['pending', 'failed', 'completed'] as const; +export type Status = (typeof statuses)[number]; diff --git a/backend/src/payment/enums/type.enum.ts b/backend/src/payment/enums/type.enum.ts new file mode 100644 index 0000000..be6ef5d --- /dev/null +++ b/backend/src/payment/enums/type.enum.ts @@ -0,0 +1,2 @@ +export const types = ['rent', 'watch', 'tip'] as const; +export type Type = (typeof types)[number]; diff --git a/backend/src/payment/guard/payment.guard.ts b/backend/src/payment/guard/payment.guard.ts new file mode 100644 index 0000000..6098ddc --- /dev/null +++ b/backend/src/payment/guard/payment.guard.ts @@ -0,0 +1,25 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class PaymentGuard implements CanActivate { + constructor( + @InjectRepository(Shareholder) + private shareholdersRepository: Repository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const filmmaker = request?.user?.filmmaker; + if (!filmmaker) return true; + const filmmakerId = filmmaker?.id; + const contentId = request.params.id; + + const shareholder = await this.shareholdersRepository.findOne({ + where: { filmmakerId, contentId }, + }); + return !shareholder; + } +} diff --git a/backend/src/payment/helpers/payment-per-period.ts b/backend/src/payment/helpers/payment-per-period.ts new file mode 100644 index 0000000..6582d0e --- /dev/null +++ b/backend/src/payment/helpers/payment-per-period.ts @@ -0,0 +1,39 @@ +import { + MAX_PAYOUT_USD, + PAYOUT_USD_PER_10_SECONDS, +} from 'src/payout/constants/amount'; +import { SubscriptionType } from 'src/subscriptions/enums/types.enum'; + +export interface AudiencePaymentAmount { + value: number; + maxPerMonth: number; +} + +export const getAudiencePaymentAmount = (audienceType: SubscriptionType) => { + switch (audienceType) { + case 'enthusiast': { + return { + value: PAYOUT_USD_PER_10_SECONDS.ENTHUSIAST, + maxPerMonth: MAX_PAYOUT_USD.ENTHUSIAST, + }; + } + + case 'film-buff': { + return { + value: PAYOUT_USD_PER_10_SECONDS.FILM_BUFF, + maxPerMonth: MAX_PAYOUT_USD.FILM_BUFF, + }; + } + + case 'cinephile': { + return { + value: PAYOUT_USD_PER_10_SECONDS.CINEPHILE, + maxPerMonth: MAX_PAYOUT_USD.CINEPHILE, + }; + } + + default: { + throw new Error('Invalid audience type.'); + } + } +}; diff --git a/backend/src/payment/payment-methods.controller.ts b/backend/src/payment/payment-methods.controller.ts new file mode 100644 index 0000000..e933a73 --- /dev/null +++ b/backend/src/payment/payment-methods.controller.ts @@ -0,0 +1,131 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { PaymentDTO } from './dto/response/payment.dto'; +import { + ApiBadRequestResponse, + ApiCreatedResponse, + ApiOkResponse, +} from '@nestjs/swagger'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import AddPaymentMethodDTO from './dto/request/add-payment-method.dto'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { PaymentMethodDTO } from './dto/response/payment-method.dto'; +import UpdatePaymentMethodDTO from './dto/request/update-payment-method.dto'; +import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator'; +import { PaymentMethodsService } from './services/payment-methods.service'; +import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard'; + +@Controller('payment-methods') +export class PaymentMethodsController { + constructor(private readonly paymentMethodsService: PaymentMethodsService) {} + + @Get() + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @ApiOkResponse({ type: [PaymentMethodDTO] }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async getPaymentMethods(@User() user: RequestUser['user']) { + try { + const paymentMethods = await this.paymentMethodsService.getPaymentMethods( + user.filmmaker.id, + ); + return paymentMethods.map((method) => new PaymentMethodDTO(method)); + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Post() + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @ApiCreatedResponse({ type: PaymentDTO }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async addPaymentMethod( + @Body() method: AddPaymentMethodDTO, + @User() user: RequestUser['user'], + ) { + try { + const paymentMethod = await this.paymentMethodsService.addPaymentMethod( + method, + user.filmmaker.id, + ); + return new PaymentMethodDTO(paymentMethod); + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Patch(':id') + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @ApiOkResponse({ type: PaymentDTO }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async updatePaymentMethod( + @Param('id') paymentMethodId: string, + @Body() method: UpdatePaymentMethodDTO, + @User() user: RequestUser['user'], + ) { + try { + const paymentMethod = + await this.paymentMethodsService.updatePaymentMethod( + paymentMethodId, + method, + user.filmmaker.id, + ); + return new PaymentMethodDTO(paymentMethod); + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Patch(':id/select') + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @ApiOkResponse({ type: PaymentDTO }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async selectPaymentMethod( + @Param('id') paymentMethodId: string, + @User() user: RequestUser['user'], + ) { + try { + const paymentMethod = + await this.paymentMethodsService.selectPaymentMethod( + paymentMethodId, + user.filmmaker.id, + ); + return paymentMethod.affected > 0; + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Delete(':id') + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @ApiOkResponse({ type: PaymentDTO }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async deletePaymentMethod( + @Param('id') paymentMethodId: string, + @User() user: RequestUser['user'], + ) { + try { + await this.paymentMethodsService.deletePaymentMethod( + paymentMethodId, + user.filmmaker.id, + ); + return { success: true }; + } catch (error) { + throw new BadRequestException(error.message); + } + } +} diff --git a/backend/src/payment/payment.controller.ts b/backend/src/payment/payment.controller.ts new file mode 100644 index 0000000..1ac8f9c --- /dev/null +++ b/backend/src/payment/payment.controller.ts @@ -0,0 +1,117 @@ +import { + BadRequestException, + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { PaymentDTO } from './dto/response/payment.dto'; +import { + ApiBadRequestResponse, + ApiCreatedResponse, + ApiOkResponse, +} from '@nestjs/swagger'; +import { PaymentService } from './services/payment.service'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator'; +import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard'; +import { UserThrottlerGuard } from 'src/common/guards/user-throttle.guard'; +import { Throttle } from '@nestjs/throttler'; +import { PaymentGuard } from './guard/payment.guard'; +import { SubscriptionType } from 'src/subscriptions/enums/types.enum'; + +@Controller('payment') +@UseGuards(HybridAuthGuard, SubscriptionsGuard) +export class PaymentController { + constructor(private readonly paymentsService: PaymentService) {} + + @Get() + @ApiOkResponse({ type: [PaymentDTO] }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async getShareholderPayments( + @Query('contentId') contentId: string, + @User() user: RequestUser['user'], + ) { + try { + const payments = await this.paymentsService.getPayments( + user.filmmaker.id, + contentId, + ); + return payments.map((payment) => new PaymentDTO(payment)); + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Get('project') + @ApiOkResponse({ type: [PaymentDTO] }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['filmmaker']) + async getShareholderPaymentsByProjectId( + @Query('ids') ids: string[] = [], + @User() user: RequestUser['user'], + ) { + try { + const payments = await this.paymentsService.getPaymentsByProjectIds( + user.filmmaker.id, + typeof ids === 'string' ? [ids] : ids, + ); + return payments.map((payment) => new PaymentDTO(payment)); + } catch (error) { + throw new BadRequestException(error.message); + } + } + + @Get('sat-price') + @ApiOkResponse({ type: Number }) + @Subscriptions(['enthusiast', 'film-buff', 'cinephile']) + async getSatPrice() { + return { price: await this.paymentsService.getSatPrice() }; + } + + @Put('send/:id') + @ApiCreatedResponse({ type: PaymentDTO }) + @ApiBadRequestResponse({ type: Error }) + @Subscriptions(['enthusiast', 'film-buff', 'cinephile']) + @UseGuards(PaymentGuard, UserThrottlerGuard) + @Throttle({ default: { limit: 60, ttl: 60_000 } }) + async sendPayment( + @Param('id') contentId: string, + @User() user: RequestUser['user'], + ) { + const subscription: SubscriptionType = user.subscriptions.find( + (sub) => + sub.type === 'enthusiast' || + sub.type === 'film-buff' || + sub.type === 'cinephile', + ).type; + + const payout = await this.paymentsService.sendPayment( + user.id, + contentId, + subscription, + ); + + return { + success: true, + usdAmount: payout.usdAmount, + milisatAmount: payout.milisatAmount, + }; + } + + @Post('validate-lightning-address') + @HttpCode(200) + @Subscriptions(['filmmaker']) + async validateLightningAddress(@Body('address') address: string) { + const valid = await this.paymentsService.validateLightningAddress(address); + return { valid }; + } +} diff --git a/backend/src/payment/payment.module.ts b/backend/src/payment/payment.module.ts new file mode 100644 index 0000000..4d49f84 --- /dev/null +++ b/backend/src/payment/payment.module.ts @@ -0,0 +1,32 @@ +import { Global, Module } from '@nestjs/common'; +import { PaymentController } from './payment.controller'; +import { IsValidAddressConstraint } from './validators/lightning-address.validator'; +import { PaymentService } from './services/payment.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Payment } from './entities/payment.entity'; +import { BTCPayService } from './providers/services/btcpay.service'; +import { PaymentMethod } from './entities/payment-method.entity'; +import { PaymentMethodsController } from './payment-methods.controller'; +import { PaymentMethodsService } from './services/payment-methods.service'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { ProjectsModule } from 'src/projects/projects.module'; +import { PayoutService } from 'src/payout/payout.service'; +import { Payout } from 'src/payout/entities/payout.entity'; + +@Global() +@Module({ + imports: [ + TypeOrmModule.forFeature([Payment, Shareholder, PaymentMethod, Payout]), + ProjectsModule, + ], + controllers: [PaymentController, PaymentMethodsController], + providers: [ + BTCPayService, + IsValidAddressConstraint, + PaymentService, + PaymentMethodsService, + PayoutService, + ], + exports: [BTCPayService, PaymentService], +}) +export class PaymentModule {} diff --git a/backend/src/payment/providers/dto/btcpay/btcpay-invoice.ts b/backend/src/payment/providers/dto/btcpay/btcpay-invoice.ts new file mode 100644 index 0000000..7511dce --- /dev/null +++ b/backend/src/payment/providers/dto/btcpay/btcpay-invoice.ts @@ -0,0 +1,78 @@ +/** + * BTCPay Greenfield API invoice response + */ +export interface BTCPayInvoiceResponse { + id: string; + storeId: string; + amount: string; + currency: string; + type: string; + checkoutLink: string; + createdTime: number; + expirationTime: number; + monitoringExpiration: number; + status: 'New' | 'Processing' | 'Settled' | 'Invalid' | 'Expired'; + additionalStatus: 'None' | 'PaidLate' | 'PaidPartial' | 'Marked' | 'Invalid' | 'PaidOver'; + metadata?: Record; +} + +/** + * BTCPay invoice payment method (contains BOLT11 for Lightning) + */ +export interface BTCPayPaymentMethod { + paymentMethod: string; + cryptoCode: string; + destination: string; // BOLT11 invoice for Lightning + paymentLink: string; + rate: string; + paymentMethodPaid: string; + totalPaid: string; + due: string; + amount: string; + networkFee: string; + payments: BTCPayPayment[]; + activated: boolean; +} + +export interface BTCPayPayment { + id: string; + receivedDate: number; + value: string; + fee: string; + status: string; + destination: string; +} + +/** + * BTCPay webhook event payload + */ +export interface BTCPayWebhookEvent { + deliveryId: string; + webhookId: string; + originalDeliveryId: string; + isRedelivery: boolean; + type: BTCPayWebhookEventType; + timestamp: number; + storeId: string; + invoiceId: string; + metadata?: Record; +} + +export type BTCPayWebhookEventType = + | 'InvoiceCreated' + | 'InvoiceReceivedPayment' + | 'InvoiceProcessing' + | 'InvoiceExpired' + | 'InvoiceSettled' + | 'InvoiceInvalid' + | 'InvoicePaymentSettled'; + +/** + * BTCPay Lightning pay response + */ +export interface BTCPayLightningPayResponse { + result: 'Ok' | 'CouldNotFindRoute' | 'Error'; + errorDetail?: string; + paymentHash?: string; + preimage?: string; +} diff --git a/backend/src/payment/providers/dto/strike/address.ts b/backend/src/payment/providers/dto/strike/address.ts new file mode 100644 index 0000000..8fc683d --- /dev/null +++ b/backend/src/payment/providers/dto/strike/address.ts @@ -0,0 +1,7 @@ +export default class Address { + country: string; + state?: string; + city: string; + postCode: string; + line1: string; +} diff --git a/backend/src/payment/providers/dto/strike/amount.ts b/backend/src/payment/providers/dto/strike/amount.ts new file mode 100644 index 0000000..8f6aa59 --- /dev/null +++ b/backend/src/payment/providers/dto/strike/amount.ts @@ -0,0 +1,4 @@ +export class Amount { + amount: string; + currency: string; +} diff --git a/backend/src/payment/providers/dto/strike/beneficiary.ts b/backend/src/payment/providers/dto/strike/beneficiary.ts new file mode 100644 index 0000000..dbe553b --- /dev/null +++ b/backend/src/payment/providers/dto/strike/beneficiary.ts @@ -0,0 +1,15 @@ +import Address from './address'; + +export class Beneficiary { + type: 'INDIVIDUAL' | 'COMPANY' = 'INDIVIDUAL'; + name: string; + address: Address; + + // Company fields + email?: string; + phoneNumber?: string; + url?: string; + + // Individual fields + dateOfBirth?: Date; +} diff --git a/backend/src/payment/providers/dto/strike/invoice-quote.ts b/backend/src/payment/providers/dto/strike/invoice-quote.ts new file mode 100644 index 0000000..f8a667e --- /dev/null +++ b/backend/src/payment/providers/dto/strike/invoice-quote.ts @@ -0,0 +1,17 @@ +import { Amount } from './amount'; + +export default class InvoiceQuote { + quoteId: string; + targetAmount: Amount; + sourceAmount: Amount; + conversionRate: { + amount: string; + sourceCurrency: string; + targetCurrency: string; + }; + description: string; + lnInvoice: string; + onchainAddress: string; + expiration: Date; + expirationInSec: number; +} diff --git a/backend/src/payment/providers/dto/strike/invoice.ts b/backend/src/payment/providers/dto/strike/invoice.ts new file mode 100644 index 0000000..543cb27 --- /dev/null +++ b/backend/src/payment/providers/dto/strike/invoice.ts @@ -0,0 +1,10 @@ +import { Amount } from './amount'; + +export default class Invoice { + invoiceId: string; + amount: Amount; + description: string; + created: Date; + state?: 'UNPAID' | 'PENDING' | 'PAID' | 'CANCELLED'; + correlationId?: string; +} diff --git a/backend/src/payment/providers/dto/strike/payment-executed.ts b/backend/src/payment/providers/dto/strike/payment-executed.ts new file mode 100644 index 0000000..9bdc7a4 --- /dev/null +++ b/backend/src/payment/providers/dto/strike/payment-executed.ts @@ -0,0 +1,12 @@ +import { Amount } from './amount'; + +export class PaymentExecuted { + paymentId: string; + state: 'PENDING' | 'COMPLETED' | 'FAILED'; + completed: string; + delivered: string; + amount: Amount; + totalFee: Amount; + lightningNetworkFee: Amount; + totalAmount: Amount; +} diff --git a/backend/src/payment/providers/dto/strike/payment-method.ts b/backend/src/payment/providers/dto/strike/payment-method.ts new file mode 100644 index 0000000..e9f071e --- /dev/null +++ b/backend/src/payment/providers/dto/strike/payment-method.ts @@ -0,0 +1,15 @@ +import Address from './address'; +import { Beneficiary } from './beneficiary'; + +export class PaymentMethod { + id: string; + status: 'PENDING' | 'READY' | 'SUSPENDED' | 'INVALID' | 'INACTIVE'; + created: Date; + transferType: 'ACH' | 'US_DOMESTIC_WIRE' = 'ACH'; + accountNumber: string; + routingNumber: string; + accountType?: 'CHECKING' | 'SAVINGS'; + bankName?: string; + bankAddress: Address; + beneficiaries: Beneficiary[]; +} diff --git a/backend/src/payment/providers/dto/strike/payment-quote.ts b/backend/src/payment/providers/dto/strike/payment-quote.ts new file mode 100644 index 0000000..949bbc7 --- /dev/null +++ b/backend/src/payment/providers/dto/strike/payment-quote.ts @@ -0,0 +1,11 @@ +import { Amount } from './amount'; + +export class PaymentQuote { + paymentQuoteId: string; + description: string; + amount: Amount; + totalAmount: Amount; + totalFee: Amount; + validUntil: string; + lightningNetworkFee: Amount; +} diff --git a/backend/src/payment/providers/dto/strike/payout.ts b/backend/src/payment/providers/dto/strike/payout.ts new file mode 100644 index 0000000..fda11d1 --- /dev/null +++ b/backend/src/payment/providers/dto/strike/payout.ts @@ -0,0 +1,11 @@ +import { Amount } from './amount'; + +export class StrikePayout { + id: string; + state: 'NEW' | 'INITIATED' | 'COMPLETED' | 'FAILED' | 'REVERSED'; + created: string; + paymentMethodId: string; + amount: Amount; + initiated?: string; + completed?: string; +} diff --git a/backend/src/payment/providers/services/btcpay.service.ts b/backend/src/payment/providers/services/btcpay.service.ts new file mode 100644 index 0000000..560c991 --- /dev/null +++ b/backend/src/payment/providers/services/btcpay.service.ts @@ -0,0 +1,351 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { LightningService } from './lightning.service'; +import { Payment } from '../../entities/payment.entity'; +import { LightningPaymentDTO } from '../../dto/response/lightning-payment.dto'; +import Invoice from '../dto/strike/invoice'; +import InvoiceQuote from '../dto/strike/invoice-quote'; +import axios from 'axios'; +import type { + BTCPayInvoiceResponse, + BTCPayPaymentMethod, + BTCPayLightningPayResponse, +} from '../dto/btcpay/btcpay-invoice'; + +/** + * BTCPay Server Greenfield API client. + * + * Replaces StrikeService as the Lightning payment provider. + * Handles invoice creation (receiving payments), Lightning payouts + * (paying creator Lightning addresses), and BTC rate fetching. + */ +@Injectable() +export class BTCPayService implements LightningService { + private readonly baseUrl: string; + private readonly storeId: string; + private readonly apiKey: string; + + constructor() { + this.baseUrl = (process.env.BTCPAY_URL || process.env.BTCPAY_SERVER_URL || '').replace(/\/+$/, ''); + this.storeId = process.env.BTCPAY_STORE_ID || ''; + this.apiKey = process.env.BTCPAY_API_KEY || ''; + + if (!this.baseUrl || !this.storeId || !this.apiKey) { + Logger.warn( + 'BTCPay Server environment variables not fully configured (BTCPAY_URL, BTCPAY_STORE_ID, BTCPAY_API_KEY)', + 'BTCPayService', + ); + } + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + private get headers() { + return { + Authorization: `token ${this.apiKey}`, + 'Content-Type': 'application/json', + }; + } + + private storeUrl(path: string): string { + return `${this.baseUrl}/api/v1/stores/${this.storeId}${path}`; + } + + // ── Invoice Creation (receiving payments from users) ───────────────────── + + /** + * Create a BTCPay invoice denominated in USD, paid via Lightning. + * Returns data shaped like Strike's Invoice DTO for drop-in compatibility + * with the existing RentsService. + */ + async issueInvoice( + amount: number, + description = 'Invoice for order', + correlationId?: string, + ): Promise { + try { + const body = { + amount: amount.toString(), + currency: 'USD', + metadata: { + orderId: correlationId || undefined, + itemDesc: description, + }, + checkout: { + paymentMethods: ['BTC-LN'], + expirationMinutes: 15, + monitoringMinutes: 60, + speedPolicy: 'HighSpeed', + }, + }; + + const { data } = await axios.post( + this.storeUrl('/invoices'), + body, + { headers: this.headers }, + ); + + // Map to the Invoice DTO the rest of the codebase expects + return { + invoiceId: data.id, + amount: { amount: data.amount, currency: data.currency }, + description, + created: new Date(data.createdTime * 1000), + state: this.mapInvoiceState(data.status), + correlationId, + }; + } catch (error) { + Logger.error('BTCPay invoice creation failed: ' + error.message, 'BTCPayService'); + throw new BadRequestException(error.message); + } + } + + /** + * Fetch the Lightning BOLT11 for an existing invoice. + * Returns data shaped like Strike's InvoiceQuote DTO. + */ + async issueQuote(invoiceId: string): Promise { + try { + // Get the invoice to know the amount + const invoice = await this.getRawInvoice(invoiceId); + + // Get payment methods to retrieve the BOLT11 + const { data: paymentMethods } = await axios.get( + this.storeUrl(`/invoices/${invoiceId}/payment-methods`), + { headers: this.headers }, + ); + + const lightning = paymentMethods.find( + (pm) => pm.paymentMethod === 'BTC-LN' || pm.paymentMethod === 'BTC-LNURL', + ); + + if (!lightning) { + throw new Error('No Lightning payment method found for this invoice'); + } + + const expirationDate = new Date(invoice.expirationTime * 1000); + const expirationInSec = Math.max( + 0, + Math.floor((expirationDate.getTime() - Date.now()) / 1000), + ); + + return { + quoteId: invoiceId, + description: invoice.metadata?.itemDesc as string || 'Invoice', + lnInvoice: lightning.destination, + onchainAddress: '', + expiration: expirationDate, + expirationInSec, + targetAmount: { + amount: invoice.amount, + currency: invoice.currency, + }, + sourceAmount: { + amount: lightning.amount, + currency: 'BTC', + }, + conversionRate: { + amount: lightning.rate, + sourceCurrency: 'BTC', + targetCurrency: invoice.currency, + }, + }; + } catch (error) { + Logger.error('BTCPay quote retrieval failed: ' + error.message, 'BTCPayService'); + throw new BadRequestException(error.message); + } + } + + /** + * Get the status of an existing invoice. + * Returns data shaped like Strike's Invoice DTO. + */ + async getInvoice(invoiceId: string): Promise { + try { + const raw = await this.getRawInvoice(invoiceId); + return { + invoiceId: raw.id, + amount: { amount: raw.amount, currency: raw.currency }, + description: (raw.metadata?.itemDesc as string) || '', + created: new Date(raw.createdTime * 1000), + state: this.mapInvoiceState(raw.status), + correlationId: raw.metadata?.orderId as string, + }; + } catch (error) { + Logger.error('BTCPay getInvoice failed: ' + error.message, 'BTCPayService'); + throw new BadRequestException(error.message); + } + } + + // ── Outgoing Lightning Payments (creator payouts) ─────────────────────── + + /** + * Pay a creator's Lightning address by resolving LNURL-pay, + * fetching a BOLT11, then paying it via BTCPay's Lightning node. + */ + async sendPaymentWithAddress( + address: string, + payment: Payment, + ): Promise { + try { + const sats = Math.floor(payment.milisatsAmount / 1000); + + // Resolve the Lightning address to a BOLT11 invoice + const bolt11 = await this.resolveLightningAddress(address, sats); + + // Pay the BOLT11 via BTCPay's internal Lightning node + const { data } = await axios.post( + this.storeUrl('/lightning/BTC/invoices/pay'), + { BOLT11: bolt11 }, + { headers: this.headers }, + ); + + if (data.result !== 'Ok') { + throw new Error( + `Lightning payment failed: ${data.result} — ${data.errorDetail || 'unknown error'}`, + ); + } + + return { + id: data.paymentHash || 'btcpay-payment', + status: 'COMPLETED', + }; + } catch (error) { + Logger.error('BTCPay sendPayment failed: ' + error.message, 'BTCPayService'); + throw new Error(error.message); + } + } + + /** + * Validate a Lightning address by attempting to resolve it. + */ + async validateAddress(address: string): Promise { + try { + const [username, domain] = address.split('@'); + if (!username || !domain) return false; + + const url = `https://${domain}/.well-known/lnurlp/${username}`; + const { status } = await axios.get(url, { timeout: 10_000 }); + return status === 200; + } catch { + return false; + } + } + + // ── Rate Fetching ─────────────────────────────────────────────────────── + + /** + * Get the current USD value of 1 satoshi. + * Uses CoinGecko as a reliable public rate source. + */ + async getSatoshiRate(): Promise { + try { + // Try BTCPay's built-in rate provider first + const { data } = await axios.get( + `${this.baseUrl}/api/v1/stores/${this.storeId}/rates`, + { headers: this.headers, timeout: 5_000 }, + ); + + const btcUsd = data?.find?.( + (r: { currencyPair: string }) => r.currencyPair === 'BTC_USD', + ); + + if (btcUsd?.rate) { + return Number(btcUsd.rate) * 0.000_000_01; // Convert BTC price to sat price + } + + throw new Error('Rate not found in BTCPay response'); + } catch { + // Fallback to CoinGecko + try { + const { data } = await axios.get( + 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd', + { timeout: 10_000 }, + ); + const btcPrice = data?.bitcoin?.usd; + if (!btcPrice) throw new Error('CoinGecko returned no BTC price'); + return btcPrice * 0.000_000_01; + } catch (fallbackError) { + Logger.error('Rate fetch failed from all sources: ' + fallbackError.message, 'BTCPayService'); + throw new BadRequestException('Could not get satoshi rate'); + } + } + } + + // ── Private Helpers ───────────────────────────────────────────────────── + + private async getRawInvoice(invoiceId: string): Promise { + const { data } = await axios.get( + this.storeUrl(`/invoices/${invoiceId}`), + { headers: this.headers }, + ); + return data; + } + + /** + * Map BTCPay invoice status to Strike-compatible state string. + */ + private mapInvoiceState( + status: BTCPayInvoiceResponse['status'], + ): Invoice['state'] { + switch (status) { + case 'Settled': + return 'PAID'; + case 'Processing': + return 'PENDING'; + case 'Expired': + case 'Invalid': + return 'CANCELLED'; + case 'New': + default: + return 'UNPAID'; + } + } + + /** + * Resolve a Lightning address (user@domain.com) to a BOLT11 invoice + * via the LNURL-pay protocol. + */ + private async resolveLightningAddress( + address: string, + amountSats: number, + ): Promise { + const [username, domain] = address.split('@'); + if (!username || !domain) { + throw new Error(`Invalid Lightning address: ${address}`); + } + + // Step 1: Fetch the LNURL-pay metadata + const metadataUrl = `https://${domain}/.well-known/lnurlp/${username}`; + const { data: lnurlData } = await axios.get(metadataUrl, { timeout: 10_000 }); + + if (lnurlData.status === 'ERROR') { + throw new Error(`LNURL error: ${lnurlData.reason}`); + } + + const amountMillisats = amountSats * 1000; + const minSendable = lnurlData.minSendable || 1000; + const maxSendable = lnurlData.maxSendable || 1_000_000_000_000; + + if (amountMillisats < minSendable || amountMillisats > maxSendable) { + throw new Error( + `Amount ${amountSats} sats is outside the allowed range (${minSendable / 1000}-${maxSendable / 1000} sats)`, + ); + } + + // Step 2: Request the BOLT11 invoice + const callbackUrl = new URL(lnurlData.callback); + callbackUrl.searchParams.set('amount', amountMillisats.toString()); + + const { data: invoiceData } = await axios.get(callbackUrl.toString(), { timeout: 10_000 }); + + if (invoiceData.status === 'ERROR') { + throw new Error(`LNURL callback error: ${invoiceData.reason}`); + } + + if (!invoiceData.pr) { + throw new Error('No payment request (BOLT11) returned from LNURL callback'); + } + + return invoiceData.pr; + } +} diff --git a/backend/src/payment/providers/services/lightning.service.ts b/backend/src/payment/providers/services/lightning.service.ts new file mode 100644 index 0000000..c02b20b --- /dev/null +++ b/backend/src/payment/providers/services/lightning.service.ts @@ -0,0 +1,13 @@ +import { LightningPaymentDTO } from '../../dto/response/lightning-payment.dto'; +import { Payment } from '../../entities/payment.entity'; + +export interface LightningService { + sendPaymentWithAddress( + address: string, + payment: Payment, + ): Promise; + + validateAddress(address: string): Promise; + + getSatoshiRate(): Promise; +} diff --git a/backend/src/payment/providers/services/strike.service.ts b/backend/src/payment/providers/services/strike.service.ts new file mode 100644 index 0000000..477baaa --- /dev/null +++ b/backend/src/payment/providers/services/strike.service.ts @@ -0,0 +1,276 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { LightningService } from './lightning.service'; +import { Payment } from '../../entities/payment.entity'; +import axios from 'axios'; +import { PaymentQuote } from '../dto/strike/payment-quote'; +import { PaymentExecuted } from '../dto/strike/payment-executed'; +import { LightningPaymentDTO } from '../../dto/response/lightning-payment.dto'; +import { PaymentMethod } from '../dto/strike/payment-method'; +import { AddBankDTO } from '../../dto/request/add-bank.dto'; +import { StrikePayout } from '../dto/strike/payout'; +import Invoice from '../dto/strike/invoice'; +import InvoiceQuote from '../dto/strike/invoice-quote'; +// Sentry removed -- errors logged via Logger + +@Injectable() +export class StrikeService implements LightningService { + STRIKE_API_URL = 'https://api.strike.me/v1'; + constructor() { + axios.defaults.headers.common['Authorization'] = + `Bearer ${process.env.STRIKE_API_KEY}`; + } + + async sendPaymentWithAddress( + address: string, + payment: Payment, + currency: 'USD' | 'BTC' = 'BTC', + ): Promise { + try { + // Transfers to Blink wallets does not support description + const isBlinkWallet = address.toLowerCase().endsWith('@blink.sv'); + + const createPaymentRequest = JSON.stringify({ + lnAddressOrUrl: address, + sourceCurrency: currency, + amount: { + amount: + currency === 'BTC' + ? payment.milisatsAmount / 1000 / 100_000_000 + : payment.usdAmount, + currency, + }, + description: isBlinkWallet ? undefined : 'Tip from IndeeHub', + }); + + const createPaymentConfig = { + method: 'post', + url: `${this.STRIKE_API_URL}/payment-quotes/lightning/lnurl`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + data: createPaymentRequest, + }; + + const { data: paymentQuote } = + await axios(createPaymentConfig); + + const executePaymentConfig = { + method: 'patch', + url: `${this.STRIKE_API_URL}/payment-quotes/${paymentQuote.paymentQuoteId}/execute`, + headers: { + Accept: 'application/json', + }, + }; + + const { data: paymentExecuted } = + await axios(executePaymentConfig); + return { + id: paymentExecuted.paymentId, + status: paymentExecuted.state, + }; + } catch (error) { + Logger.error('Payment could not be sent: ' + error.message); + throw new Error(error.message); + } + } + + async validateAddress(address: string): Promise { + // url encode the address + const encodedAddress = encodeURIComponent(address); + try { + const config = { + method: 'get', + url: `${this.STRIKE_API_URL}/payment-quotes/lightning/lnurl/${encodedAddress}`, + headers: { + Accept: 'application/json', + }, + }; + const { status } = await axios(config); + return status === 200; + } catch { + return false; + } + } + + async getSatoshiRate(): Promise { + const config = { + method: 'get', + url: `${this.STRIKE_API_URL}/rates/ticker`, + headers: { + Accept: 'application/json', + }, + }; + try { + const { data: ticker } = await axios(config); + const btcUsdTicker = ticker.find( + (ticker) => + ticker.sourceCurrency === 'BTC' && ticker.targetCurrency === 'USD', + ); + if (!btcUsdTicker?.amount) { + throw new BadRequestException('Could not get satoshi rate'); + } + return Number(btcUsdTicker.amount) * 0.000_000_01; + } catch (error) { + Logger.error({ error }); + throw new BadRequestException(error.message); + } + } + + async addPaymentMethod(addPaymentMethodDTO: AddBankDTO) { + try { + const config = { + method: 'post', + url: `${this.STRIKE_API_URL}/payment-methods/bank`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + data: JSON.stringify(addPaymentMethodDTO), + }; + + const { data: paymentMethod } = await axios(config); + Logger.debug('Payment method added! ID: ' + paymentMethod.id); + return paymentMethod; + } catch (error) { + Logger.error({ error }); + throw new BadRequestException(error.message); + } + } + + async updatePaymentMethod(id: string, updatePaymentMethodDTO: AddBankDTO) { + try { + const config = { + method: 'patch', + url: `${this.STRIKE_API_URL}/payment-methods/bank/${id}`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + data: JSON.stringify(updatePaymentMethodDTO), + }; + + const { data: paymentMethod } = await axios(config); + Logger.debug('Payment method updated! ID: ' + paymentMethod.id); + return paymentMethod; + } catch (error) { + Logger.error({ error }); + throw new BadRequestException(error.message); + } + } + + async getPaymentMethod(id: string) { + const config = { + method: 'get', + url: `${this.STRIKE_API_URL}/payment-methods/bank/${id}`, + headers: { + Accept: 'application/json', + }, + }; + + const { data: paymentMethod } = await axios(config); + return paymentMethod; + } + + async createAndExecutePayout(paymentMethodId: string, amount: string) { + try { + const createPaymentRequest = JSON.stringify({ + paymentMethodId, + amount, + }); + + const createPaymentConfig = { + method: 'post', + url: `${this.STRIKE_API_URL}/payouts`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + data: createPaymentRequest, + }; + + const { data: payout } = await axios(createPaymentConfig); + + const initiatePaymentConfig = { + method: 'patch', + url: `${this.STRIKE_API_URL}/payouts/${payout.id}/initiate`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const { data: initiated } = await axios( + initiatePaymentConfig, + ); + return initiated; + } catch (error) { + Logger.error('Payout could not be sent: ' + error.message); + throw new Error(error.message); + } + } + + async issueInvoice( + amount: number, + description: string = 'Invoice for order', + correlationId?: string, + ) { + try { + const invoiceRequest = JSON.stringify({ + correlationId, + description, + amount: { + amount, + currency: 'USD', + }, + }); + + const invoiceConfig = { + method: 'post', + url: `${this.STRIKE_API_URL}/invoices`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + data: invoiceRequest, + }; + + const { data: invoice } = await axios(invoiceConfig); + return invoice; + } catch (error) { + Logger.error({ error }); + throw new BadRequestException(error.message); + } + } + + async issueQuote(invoiceId: string) { + try { + const quoteConfig = { + method: 'post', + url: `${this.STRIKE_API_URL}/invoices/${invoiceId}/quote`, + headers: { + Accept: 'application/json', + }, + }; + + const { data: quote } = await axios(quoteConfig); + return quote; + } catch (error) { + Logger.error({ error }); + throw new BadRequestException(error.message); + } + } + + async getInvoice(invoiceId: string) { + const config = { + method: 'get', + url: `${this.STRIKE_API_URL}/invoices/${invoiceId}`, + headers: { + Accept: 'application/json', + }, + }; + + const { data: invoice } = await axios(config); + return invoice; + } +} diff --git a/backend/src/payment/services/payment-methods.service.ts b/backend/src/payment/services/payment-methods.service.ts new file mode 100644 index 0000000..aedb8c8 --- /dev/null +++ b/backend/src/payment/services/payment-methods.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { randomUUID } from 'node:crypto'; + +import { BTCPayService } from '../providers/services/btcpay.service'; +import AddPaymentMethodDTO from '../dto/request/add-payment-method.dto'; +import { PaymentMethod } from '../entities/payment-method.entity'; +import UpdatePaymentMethodDTO from '../dto/request/update-payment-method.dto'; + +@Injectable() +export class PaymentMethodsService { + constructor( + private readonly btcpayService: BTCPayService, + @InjectRepository(PaymentMethod) + private paymentMethodsRepository: Repository, + ) {} + + async addPaymentMethod( + paymentMethod: AddPaymentMethodDTO, + filmmakerId: string, + ) { + // Lightning-only: validate and save Lightning address + const existingMethod = await this.paymentMethodsRepository.findOne({ + where: { + filmmakerId, + type: 'LIGHTNING', + }, + }); + + if ( + existingMethod && + existingMethod.lightningAddress !== paymentMethod.lightningAddress + ) { + await this.paymentMethodsRepository.softDelete({ + id: existingMethod.id, + }); + } + + const savedMethod = + existingMethod && + existingMethod.lightningAddress === paymentMethod.lightningAddress + ? existingMethod + : await this.paymentMethodsRepository.save({ + id: randomUUID(), + filmmakerId, + lightningAddress: paymentMethod.lightningAddress, + type: 'LIGHTNING', + withdrawalFrequency: paymentMethod.withdrawalFrequency, + }); + + await this.paymentMethodsRepository.save(savedMethod); + await this.selectPaymentMethod(savedMethod.id, filmmakerId); + + return { + ...savedMethod, + selected: true, + }; + } + + async selectPaymentMethod(paymentMethodId: string, filmmakerId: string) { + await this.paymentMethodsRepository.update( + { + filmmakerId, + }, + { + selected: false, + }, + ); + + return await this.paymentMethodsRepository.update( + { + id: paymentMethodId, + filmmakerId, + }, + { + selected: true, + }, + ); + } + + async getPaymentMethods(filmmakerId: string) { + return await this.paymentMethodsRepository.find({ + where: { + filmmakerId, + }, + order: { + type: 'DESC', + selected: 'DESC', + }, + }); + } + + async updatePaymentMethod( + paymentMethodId: string, + updatePaymentMethod: UpdatePaymentMethodDTO, + filmmakerId: string, + ) { + await this.paymentMethodsRepository.update( + { + id: paymentMethodId, + filmmakerId, + }, + { + lightningAddress: updatePaymentMethod.lightningAddress, + withdrawalFrequency: updatePaymentMethod.withdrawalFrequency, + selected: true, + }, + ); + + await this.selectPaymentMethod(paymentMethodId, filmmakerId); + + return await this.paymentMethodsRepository.findOne({ + where: { + id: paymentMethodId, + filmmakerId, + }, + }); + } + + async deletePaymentMethod(paymentMethodId: string, filmmakerId: string) { + await this.paymentMethodsRepository.softDelete({ + id: paymentMethodId, + filmmakerId, + }); + } +} diff --git a/backend/src/payment/services/payment.service.ts b/backend/src/payment/services/payment.service.ts new file mode 100644 index 0000000..241d4c4 --- /dev/null +++ b/backend/src/payment/services/payment.service.ts @@ -0,0 +1,358 @@ +import { + BadRequestException, + Inject, + Injectable, + Logger, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Payment } from '../entities/payment.entity'; +import { + Between, + FindManyOptions, + In, + IsNull, + MoreThanOrEqual, + Not, + Repository, +} from 'typeorm'; +import { randomUUID } from 'node:crypto'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { BTCPayService } from '../providers/services/btcpay.service'; +import { PaymentMethod } from '../entities/payment-method.entity'; +import { MailService } from 'src/mail/mail.service'; +import * as moment from 'moment'; +import { LightningService } from '../providers/services/lightning.service'; +import { Type } from '../enums/type.enum'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { Frequency } from '../enums/frequency.enum'; +import { ProjectsService } from 'src/projects/projects.service'; +import { Content } from 'src/contents/entities/content.entity'; +import { Status } from '../enums/status.enum'; +import { PayoutService } from 'src/payout/payout.service'; +import { getAudiencePaymentAmount } from '../helpers/payment-per-period'; +import { SubscriptionType } from 'src/subscriptions/enums/types.enum'; + +@Injectable() +export class PaymentService { + provider: LightningService; + + constructor( + private readonly btcpayService: BTCPayService, + @InjectRepository(Payment) + private paymentsRepository: Repository, + @InjectRepository(PaymentMethod) + private paymentMethodsRepository: Repository, + @InjectRepository(Shareholder) + private shareholdersRepository: Repository, + @Inject(MailService) + private mailService: MailService, + @Inject(ProjectsService) + private projectsService: ProjectsService, + @Inject(PayoutService) + private payoutService: PayoutService, + ) { + this.provider = this.btcpayService; + } + + async getPayments( + filmmakerId: string, + contentId: string, + ): Promise { + const shareholders = await this.shareholdersRepository.find({ + where: { + filmmakerId, + contentId, + }, + select: ['id'], + }); + + const shareholderIds = shareholders.map((shareholder) => shareholder.id); + + return await this.getPaymentsByShareholderIds(shareholderIds); + } + + async getPaymentsByProjectIds( + filmmakerId: string, + projectIds: string[], + ): Promise { + let allProjects = []; + if (projectIds.length === 0) { + allProjects = await this.projectsService.findAll({ + filmmakerId: filmmakerId, + limit: undefined, + offset: 0, + show: 'shareholder', + }); + } + + const ids = + projectIds.length === 0 + ? allProjects.map((project) => project.id) + : projectIds; + + const shareholders = await this.shareholdersRepository + .createQueryBuilder('shareholder') + .select('DISTINCT shareholder.id', 'id') + .innerJoin(Content, 'content', 'shareholder.content_id = content.id') + .where('shareholder.filmmaker_id = :filmmakerId', { filmmakerId }) + .andWhere('content.project_id IN (:...projectIds)', { projectIds: ids }) + .getRawMany(); + const shareholderIds = shareholders.map((shareholder) => shareholder.id); + + return await this.getPaymentsByShareholderIds(shareholderIds, { + statuses: ['completed'], + order: 'DESC', + }); + } + + async getPaymentsByShareholderIds( + shareholderIds: string[], + options: { + order?: 'ASC' | 'DESC'; + statuses?: Status[]; + } = {}, + ): Promise { + const payments = await this.paymentsRepository.find({ + where: { + shareholderId: In(shareholderIds), + paymentMethodId: Not(IsNull()), + status: options?.statuses ? In(options.statuses) : undefined, + }, + relations: ['paymentMethod'], + order: { createdAt: options?.order }, + }); + + return payments.filter((payment) => payment.paymentMethod); + } + + async getSatPrice(): Promise { + return await this.provider.getSatoshiRate(); + } + + async sendPayment( + userId: string, + contentId: string, + subscription: SubscriptionType, + ): Promise<{ usdAmount: number; milisatAmount: number }> { + const amount = getAudiencePaymentAmount(subscription); + + await this.payoutService.addAmountToCurrentMonthPayout( + userId, + amount.value, + amount.maxPerMonth, + ); + await this.shareholdersRepository.update( + { + contentId, + deletedAt: IsNull(), + }, + { + pendingRevenue: () => + `pending_revenue + (cast(share as decimal) / 100.00 * ${amount.value})`, + }, + ); + + const satPrice = await this.getSatPrice(); + return { + usdAmount: amount.value, + milisatAmount: Number((amount.value / satPrice) * 1000), + }; + } + + @Cron(CronExpression.EVERY_MINUTE) + async handlePaymentsCron() { + if ( + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ) + return; + await Promise.all([ + this.handlePayment('watch'), + this.handlePayment('rent'), + ]); + Logger.log('Payments handled'); + } + + @Cron(CronExpression.EVERY_DAY_AT_10AM) + async handleDailyPayments() { + if ( + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ) + return; + await Promise.all([ + this.handlePayment('watch', 'daily'), + this.handlePayment('rent', 'daily'), + ]); + } + + @Cron(CronExpression.EVERY_WEEK) + async handleWeeklyPayments() { + if ( + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ) + return; + await Promise.all([ + this.handlePayment('watch', 'weekly'), + this.handlePayment('rent', 'weekly'), + ]); + } + + @Cron(CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_NOON) + async handleMonthlyPayments() { + if ( + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ) + return; + await Promise.all([ + this.handlePayment('watch', 'monthly'), + this.handlePayment('rent', 'monthly'), + ]); + } + + async handlePayment( + type: 'watch' | 'rent' = 'watch', + frequency: Frequency = 'automatic', + ) { + const satoshiRate = await this.provider.getSatoshiRate(); + const column = type === 'watch' ? 'pendingRevenue' : 'rentPendingRevenue'; + const options: FindManyOptions = { + where: { + [column]: MoreThanOrEqual(satoshiRate), + filmmaker: { + paymentMethods: { + type: 'LIGHTNING', + selected: true, + withdrawalFrequency: frequency, + }, + }, + }, + order: { + pendingRevenue: 'DESC', + }, + relations: ['filmmaker', 'filmmaker.paymentMethods'], + take: 5, + }; + + const shareholders = await this.shareholdersRepository.find(options); + + for (const shareholder of shareholders) { + const selectedPaymentMethod = shareholder.filmmaker.paymentMethods.find( + (method) => method.selected, + ); + if (selectedPaymentMethod?.lightningAddress) + await this.sendLightningPaymentToShareholder( + shareholder, + satoshiRate, + type, + ); + } + } + + async sendLightningPaymentToShareholder( + shareholder: Shareholder, + satoshiRate: number, + type: Type, + ) { + const revenue = + type === 'watch' + ? shareholder.pendingRevenue + : shareholder.rentPendingRevenue; + const sats = revenue / satoshiRate; + + const rounded = Math.floor(sats); + + if (rounded <= 0) { + return; + } + + const missing = sats - rounded; + const missingRevenue = missing * satoshiRate; + const revenueToBeSent = revenue - missingRevenue; + + const selectedLightningAddress = shareholder.filmmaker.paymentMethods.find( + (method) => method.selected && method.type === 'LIGHTNING', + ); + + const payment = this.paymentsRepository.create({ + id: randomUUID(), + shareholderId: shareholder.id, + status: 'pending', + milisatsAmount: rounded * 1000, + paymentMethodId: selectedLightningAddress.id, + usdAmount: revenueToBeSent, + providerId: 'TBD', + type, + }); + + const existingPayment = await this.paymentsRepository.exists({ + where: { + shareholderId: shareholder.id, + status: 'pending', + type, + createdAt: MoreThanOrEqual(moment().subtract(1, 'minute').toDate()), + }, + }); + + if (existingPayment) { + return; + } else { + await this.paymentsRepository.save(payment); + } + + try { + const providerPayment = await this.provider.sendPaymentWithAddress( + selectedLightningAddress.lightningAddress, + payment, + ); + + payment.providerId = providerPayment.id; + payment.status = 'completed'; + + await (type === 'watch' + ? this.shareholdersRepository.update( + { id: shareholder.id }, + { + pendingRevenue: () => `pending_revenue - ${revenueToBeSent}`, + updatedAt: () => 'updated_at', + }, + ) + : this.shareholdersRepository.update( + { id: shareholder.id }, + { + rentPendingRevenue: () => + `rent_pending_revenue - ${revenueToBeSent}`, + updatedAt: () => 'updated_at', + }, + )); + } catch { + payment.status = 'failed'; + } finally { + await this.paymentsRepository.save(payment); + } + } + + async getPaymentsByDate( + shareholderIds: string[], + date?: Date, + ): Promise { + const createdAt = date ? Between(date, new Date()) : undefined; + const payments = await this.paymentsRepository.find({ + where: { + shareholderId: In(shareholderIds), + createdAt, + status: 'completed', + }, + order: { + createdAt: 'ASC', + }, + }); + return payments; + } + + async validateLightningAddress(address: string) { + return this.provider.validateAddress(address); + } +} diff --git a/backend/src/payment/validators/lightning-address.validator.ts b/backend/src/payment/validators/lightning-address.validator.ts new file mode 100644 index 0000000..751befe --- /dev/null +++ b/backend/src/payment/validators/lightning-address.validator.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; + +import { + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + registerDecorator, +} from 'class-validator'; +import { BTCPayService } from '../providers/services/btcpay.service'; + +export function IsValidAddress(validationOptions?: ValidationOptions) { + return function (object: any, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + validator: IsValidAddressConstraint, + }); + }; +} + +@ValidatorConstraint({ name: 'IsValidAddressConstraint', async: true }) +@Injectable() +export class IsValidAddressConstraint implements ValidatorConstraintInterface { + constructor(private readonly btcpayService: BTCPayService) {} + + async validate(value: string): Promise { + if (!value) return true; + const valid = await this.btcpayService.validateAddress(value); + return valid; + } +} diff --git a/backend/src/payout/constants/amount.ts b/backend/src/payout/constants/amount.ts new file mode 100644 index 0000000..44c2ea6 --- /dev/null +++ b/backend/src/payout/constants/amount.ts @@ -0,0 +1,13 @@ +// Payout in USD per 10 seconds +export const PAYOUT_USD_PER_10_SECONDS = { + ENTHUSIAST: 0.000_694, + FILM_BUFF: 0.001_39, + CINEPHILE: 0.002_78, +} as const; + +// Maximun Payout in USD per month per tier +export const MAX_PAYOUT_USD = { + ENTHUSIAST: 10, + FILM_BUFF: 15, + CINEPHILE: 25, +} as const; diff --git a/backend/src/payout/entities/payout.entity.ts b/backend/src/payout/entities/payout.entity.ts new file mode 100644 index 0000000..0bc1d60 --- /dev/null +++ b/backend/src/payout/entities/payout.entity.ts @@ -0,0 +1,45 @@ +import { ColumnNumericTransformer } from 'src/database/transformers/column-numeric-transformer'; +import { User } from 'src/users/entities/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('payouts') +export class Payout { + @PrimaryColumn() + id: string; + + @Column({ + type: 'decimal', + precision: 20, + scale: 10, + default: 0, + transformer: new ColumnNumericTransformer(), + }) + amount: number; + + @Column() + userId: string; + + @Column({ type: 'timestamptz' }) + periodStart?: Date; + + @Column({ type: 'timestamptz' }) + periodEnd?: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => User, (user) => user.subscriptions) + @JoinColumn({ name: 'user_id' }) + user?: User; +} diff --git a/backend/src/payout/payout.service.ts b/backend/src/payout/payout.service.ts new file mode 100644 index 0000000..15246b1 --- /dev/null +++ b/backend/src/payout/payout.service.ts @@ -0,0 +1,45 @@ +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { Payout } from './entities/payout.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { MoreThan, Repository } from 'typeorm'; +import * as dayjs from 'dayjs'; +import { randomUUID } from 'node:crypto'; + +@Injectable() +export class PayoutService { + constructor( + @InjectRepository(Payout) + private readonly payoutRepository: Repository, + ) {} + + async addAmountToCurrentMonthPayout( + userId: string, + amountToAdd: number, + maxPayout: number, + ): Promise { + const now = dayjs().toDate(); + + let payout = await this.payoutRepository.findOne({ + where: { + userId, + periodEnd: MoreThan(now), + }, + }); + + if (!payout) { + payout = this.payoutRepository.create({ + id: randomUUID(), + userId, + periodStart: now, + periodEnd: dayjs().add(1, 'month').toDate(), + amount: 0, + }); + } + + payout.amount = Number(payout.amount) + amountToAdd; + if (payout.amount > maxPayout) { + throw new UnprocessableEntityException('Max payout reached'); + } + return await this.payoutRepository.save(payout); + } +} diff --git a/backend/src/posthog/entities/posthog-event.entity.ts b/backend/src/posthog/entities/posthog-event.entity.ts new file mode 100644 index 0000000..6cbe6a4 --- /dev/null +++ b/backend/src/posthog/entities/posthog-event.entity.ts @@ -0,0 +1,38 @@ +import { Column, Entity, PrimaryColumn, Unique } from 'typeorm'; + +@Entity('posthog-events') +export class PostHogEvents { + @PrimaryColumn({ type: 'varchar', length: 200 }) + @Unique('id', ['uuid']) + uuid: string; // The unique ID of the event within PostHog + + @Column({ type: 'varchar', length: 200 }) + event: string; // The name of the event that was sent + + @Column('jsonb') + properties: object; // A JSON object with all the properties sent along with an event + + @Column('jsonb', { nullable: true }) + elements: object | null; // This field is present for backwards compatibility but has been deprecated + + @Column({ nullable: true, type: 'jsonb' }) + set: object; // A JSON object with any person properties sent with the $set field + + @Column({ nullable: true, type: 'jsonb' }) + setOnce: object | null; // A JSON object with any person properties sent with the $set_once field + + @Column({ type: 'varchar', length: 200 }) + distinctId: string; // The distinct_id of the user who sent the event + + @Column({ type: 'integer' }) + teamId: number; // The team_id for the event + + @Column({ type: 'varchar', length: 200 }) + ip: string; // The IP address that was sent with the event + + @Column({ nullable: true, type: 'varchar', length: 200 }) + siteUrl: string | null; // This field is present for backwards compatibility but has been deprecated + + @Column({ type: 'timestamp with time zone' }) + timestamp: Date; // The timestamp associated with an event +} diff --git a/backend/src/posthog/posthog.module.ts b/backend/src/posthog/posthog.module.ts new file mode 100644 index 0000000..a8161b1 --- /dev/null +++ b/backend/src/posthog/posthog.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { PosthogService } from './posthog.service'; + +/** + * PostHog module stub. + * Removed the second PostgreSQL database connection (DATABASE_POSTHOG). + * PosthogService is kept but operates as a no-op. + * Can be replaced with self-hosted PostHog or Umami later. + */ +@Module({ + providers: [PosthogService], + exports: [PosthogService], +}) +export class PostHogModule {} diff --git a/backend/src/posthog/posthog.service.ts b/backend/src/posthog/posthog.service.ts new file mode 100644 index 0000000..681a38e --- /dev/null +++ b/backend/src/posthog/posthog.service.ts @@ -0,0 +1,64 @@ +import { Injectable, Logger } from '@nestjs/common'; + +/** + * PostHog service stub. + * All methods return empty/zero data. + * The original queried a separate PostHog PostgreSQL database. + * Can be replaced with self-hosted PostHog or Umami later. + */ +@Injectable() +export class PosthogService { + private readonly logger = new Logger(PosthogService.name); + + async getViewsByProjectIds( + projectIds: string[], + startDate: Date, + endDate: Date, + ) { + return [ + { date: startDate, views: 0 }, + { date: endDate, views: 0 }, + ]; + } + + async getTotalWatchTimeByDateProjectIds( + projectIds: string[], + startDate: Date, + endDate: Date, + ) { + return [ + { date: startDate, total_watch_time: 0 }, + { date: endDate, total_watch_time: 0 }, + ]; + } + + async getTotalWatchTimeByProjectIds(projectIds: string[]) { + return 0; + } + + async getAverageWatchTimeByProjectIds( + projectIds: string[], + startDate: Date, + endDate: Date, + ) { + return [ + { date: startDate, averageWatchtTimePerUser: 0 }, + { date: endDate, averageWatchtTimePerUser: 0 }, + ]; + } + + async getSatoshisByDateProjectIds( + projectIds: string[], + startDate: Date, + endDate: Date, + ) { + return [ + { date: startDate, totalSatAmount: 0 }, + { date: endDate, totalSatAmount: 0 }, + ]; + } + + async getTrailerViewsByProjectIds(projectIds: string[]) { + return 0; + } +} diff --git a/backend/src/projects/decorators/roles.decorator.ts b/backend/src/projects/decorators/roles.decorator.ts new file mode 100644 index 0000000..911031c --- /dev/null +++ b/backend/src/projects/decorators/roles.decorator.ts @@ -0,0 +1,4 @@ +import { Reflector } from '@nestjs/core'; +import { MemberType } from '../enums/role.enum'; + +export const Roles = Reflector.createDecorator(); diff --git a/backend/src/projects/dto/request/admin-validation.dto.ts b/backend/src/projects/dto/request/admin-validation.dto.ts new file mode 100644 index 0000000..9567902 --- /dev/null +++ b/backend/src/projects/dto/request/admin-validation.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsOptional, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { Status, statuses } from 'src/projects/enums/status.enum'; +import { AddPermissionDTO } from './update-project.dto'; + +export class AdminValidationDTO { + @ApiProperty() + @IsString() + @MaxLength(100) + rejectedReason: string = ''; + + @IsOptional() + @ApiProperty() + @Type(() => String) + @IsEnum(statuses) + status: Status; + + @IsOptional() + @ValidateNested() + @ApiProperty({ type: () => AddPermissionDTO }) + @Type(() => AddPermissionDTO) + permissions: AddPermissionDTO[]; +} diff --git a/backend/src/projects/dto/request/create-project.dto.ts b/backend/src/projects/dto/request/create-project.dto.ts new file mode 100644 index 0000000..fb2a3d7 --- /dev/null +++ b/backend/src/projects/dto/request/create-project.dto.ts @@ -0,0 +1,14 @@ +import { IsEnum, IsString, MaxLength } from 'class-validator'; +import { Type, types } from 'src/projects/enums/type.enum'; + +export class CreateProjectDTO { + @IsString() + @MaxLength(100) + name: string; + + @IsString() + @IsEnum(types, { + message: `Type must be one of the following: ${types.join(', ')}`, + }) + type: Type; +} diff --git a/backend/src/projects/dto/request/list-projects.dto.ts b/backend/src/projects/dto/request/list-projects.dto.ts new file mode 100644 index 0000000..9d54939 --- /dev/null +++ b/backend/src/projects/dto/request/list-projects.dto.ts @@ -0,0 +1,108 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsEnum, IsOptional } from 'class-validator'; +import { Category, categories } from 'src/projects/enums/category.enum'; +import { Format, formats } from 'src/projects/enums/format.enum'; +import { Status, statuses } from 'src/projects/enums/status.enum'; +import { Type as TypeEnum, types } from 'src/projects/enums/type.enum'; + +export class ListProjectsDTO { + @ApiProperty() + @IsOptional() + @Type(() => String) + search?: string; + + @ApiProperty() + @IsOptional() + @Type(() => String) + @IsEnum(statuses) + status?: Status; + + @ApiProperty() + @IsOptional() + @Type(() => String) + filmmakerId?: string; + + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; + + @ApiProperty() + @IsOptional() + sort?: string; + + @ApiProperty() + @IsOptional() + randomSeed?: number; + + @ApiProperty() + @IsOptional() + order?: 'ASC' | 'DESC' = 'DESC'; + + @ApiProperty() + @IsOptional() + show?: 'all' | 'owner' | 'cast-crew' | 'shareholder' = 'all'; + + @ApiProperty() + @IsOptional() + @Transform(({ value }) => { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return; + } + } + return value; + }) + genres?: { ids?: string[]; all?: boolean }; + + @ApiProperty() + @IsOptional() + @Transform(({ value }) => { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return; + } + } + return value; + }) + subgenres?: { ids?: string[]; all?: boolean }; + + @ApiProperty() + @IsOptional() + festivals?: string[]; + + @ApiProperty() + @IsOptional() + @IsEnum(types) + type?: TypeEnum; + + @ApiProperty() + @IsOptional() + @IsEnum(formats) + format?: Format; + + @ApiProperty() + @IsOptional() + @IsEnum(categories) + category?: Category; + + @ApiProperty() + @IsOptional() + awards?: boolean; + + @ApiProperty() + @IsOptional() + screenings?: boolean; + + @ApiProperty() + @IsOptional() + relations?: boolean; +} diff --git a/backend/src/projects/dto/request/revenue-projects.dto.ts b/backend/src/projects/dto/request/revenue-projects.dto.ts new file mode 100644 index 0000000..c88b3cd --- /dev/null +++ b/backend/src/projects/dto/request/revenue-projects.dto.ts @@ -0,0 +1,14 @@ +import { Transform } from 'class-transformer'; +import { IsOptional, IsString } from 'class-validator'; +import { FilterDateRange } from 'src/common/enums/filter-date-range.enum'; + +export class ProjectsRevenueDTO { + @IsOptional() + @Transform(({ value }) => (Array.isArray(value) ? value : [value])) + @IsString({ each: true }) + ids: string[] = []; + + @IsOptional() + @IsString() + dateRange: FilterDateRange = 'since_uploaded'; +} diff --git a/backend/src/projects/dto/request/update-project.dto.ts b/backend/src/projects/dto/request/update-project.dto.ts new file mode 100644 index 0000000..0c344e7 --- /dev/null +++ b/backend/src/projects/dto/request/update-project.dto.ts @@ -0,0 +1,193 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateProjectDTO } from './create-project.dto'; +import { + IsArray, + IsEmail, + IsEnum, + IsNumber, + IsOptional, + IsString, + IsUUID, + Matches, + Max, + MaxLength, + Min, + Validate, + ValidateNested, +} from 'class-validator'; +import { Role, roles } from 'src/projects/enums/role.enum'; +import { Status, statuses } from 'src/projects/enums/status.enum'; +import { Unique } from 'typeorm'; +import { Type } from 'class-transformer'; +import { UpdateContentDTO } from 'src/contents/dto/request/update-content.dto'; +import { Format, formats } from 'src/projects/enums/format.enum'; +import { Category, categories } from 'src/projects/enums/category.enum'; +import { UpdateSeasonDto } from 'src/season/dto/request/update-season.dto.entity'; + +export class UpdateProjectDTO extends PartialType(CreateProjectDTO) { + @ApiProperty() + @IsString() + @IsOptional() + @MaxLength(100) + title?: string; + + @ApiProperty() + @IsString() + @IsOptional() + @MaxLength(100) + @Matches(/^[\da-z-]+$/) + @Validate(Unique, ['Project']) + slug?: string; + + @ApiProperty() + @IsString() + @MaxLength(350) + @IsOptional() + synopsis?: string; + + @ApiProperty() + @IsString() + @IsOptional() + trailer?: string; + + @ApiProperty() + @IsString() + @IsOptional() + poster?: string; + + @ApiProperty() + @IsOptional() + @IsUUID() + genreId?: string; + + @IsString() + @IsEnum(statuses) + @IsOptional() + status: Status = 'draft'; + + @IsString() + @IsEnum(formats) + @IsOptional() + format?: Format; + + @IsString() + @IsEnum(categories) + @IsOptional() + category?: Category; + + @ValidateNested() + @ApiProperty({ type: () => AddPermissionDTO }) + @Type(() => AddPermissionDTO) + permissions: AddPermissionDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddDocumentDTO }) + @Type(() => AddDocumentDTO) + @IsOptional() + documents?: AddDocumentDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddAwardDTO }) + @Type(() => AddAwardDTO) + @IsOptional() + awards?: AddAwardDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => AddFestivalDTO }) + @Type(() => AddFestivalDTO) + @IsOptional() + screenings?: AddFestivalDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => UpdateContentDTO }) + @Type(() => UpdateContentDTO) + @IsOptional() + film?: UpdateContentDTO; + + @ValidateNested() + @ApiProperty({ type: () => UpdateContentDTO }) + @Type(() => UpdateContentDTO) + @IsOptional() + episodes?: UpdateContentDTO[]; + + @ValidateNested() + @ApiProperty({ type: () => UpdateSeasonDto }) + @Type(() => UpdateSeasonDto) + @IsOptional() + seasons?: UpdateSeasonDto[]; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + subgenres: string[]; +} + +export class AddPermissionDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsEnum(roles) + role: Role; + + @ApiProperty() + @IsEmail() + @IsOptional() + email?: string; +} + +class AddDocumentDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsString() + name: string; + + @ApiProperty() + @IsString() + @IsOptional() + url?: string; +} + +class AddAwardDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsString() + awardIssuerId: string; + + @ApiProperty() + @IsString() + name: string; + + @ApiProperty() + @IsNumber() + @Max(2100) + @Min(1900) + year: number; +} + +class AddFestivalDTO { + @ApiProperty() + @IsString() + @IsOptional() + id?: string; + + @ApiProperty() + @IsString() + festivalId: string; + + @ApiProperty() + @IsNumber() + @Max(2100) + @Min(1900) + year: number; +} diff --git a/backend/src/projects/dto/response/award.dto.ts b/backend/src/projects/dto/response/award.dto.ts new file mode 100644 index 0000000..730d14b --- /dev/null +++ b/backend/src/projects/dto/response/award.dto.ts @@ -0,0 +1,20 @@ +import { AwardIssuerDTO } from 'src/award-issuers/dto/response/award-issuer.dto'; +import { Award } from 'src/projects/entities/award.entity'; + +export class AwardDTO { + id: string; + issuer: AwardIssuerDTO; + name: string; + year: number; + createdAt: Date; + updatedAt: Date; + + constructor(award: Award) { + this.id = award.id; + if (award.issuer) this.issuer = new AwardIssuerDTO(award.issuer); + this.name = award.name; + this.year = award.year; + this.createdAt = award.createdAt; + this.updatedAt = award.updatedAt; + } +} diff --git a/backend/src/projects/dto/response/base-project.dto.ts b/backend/src/projects/dto/response/base-project.dto.ts new file mode 100644 index 0000000..e280b4c --- /dev/null +++ b/backend/src/projects/dto/response/base-project.dto.ts @@ -0,0 +1,97 @@ +import { Format } from 'src/projects/enums/format.enum'; +import { Status } from 'src/projects/enums/status.enum'; +import { AwardDTO } from './award.dto'; +import { ScreeningDTO } from './screening.dto'; +import { Project } from 'src/projects/entities/project.entity'; +import { Type } from 'src/projects/enums/type.enum'; +import { Category } from 'src/projects/enums/category.enum'; +import { BaseContentDTO } from 'src/contents/dto/response/base-content.dto'; +import { SubgenreDTO } from 'src/genres/dto/response/subgenre.dto'; +import { GenreDTO } from 'src/genres/dto/response/genre.dto'; +import { ContentStatus } from 'src/contents/enums/content-status.enum'; +import { + getPublicS3Url, + getTrailerTranscodedFileRoute, +} from 'src/common/helper'; + +export class BaseProjectDTO { + id: string; + name: string; + title: string; + slug?: string; + synopsis?: string; + trailer?: string; + poster?: string; + status: Status; + type: Type; + format?: Format; + category?: Category; + screenings: ScreeningDTO[]; + awards: AwardDTO[]; + genre?: GenreDTO; + subgenres: SubgenreDTO[]; + film?: BaseContentDTO; + episodes?: BaseContentDTO[]; + createdAt: Date; + updatedAt: Date; + trailerStatus?: ContentStatus; + + constructor(project: Project) { + this.id = project.id; + this.name = project.name; + this.title = project.title; + this.slug = project.slug; + this.synopsis = project.synopsis; + this.type = project.type; + this.format = project.format; + this.category = project.category; + + this.status = + project.contents?.some((content) => content.status != 'completed') && + project.status === 'published' + ? 'under-review' + : project.status; + + if (this.type === 'episodic') { + const contents = + project.contents?.filter((content) => content.status === 'completed') || + []; + this.episodes = contents.map((content) => new BaseContentDTO(content)); + } else { + this.film = + project.contents?.length > 0 + ? new BaseContentDTO(project.contents[0]) + : undefined; + } + + this.genre = project.genre ? new GenreDTO(project.genre) : undefined; + + this.createdAt = project.createdAt; + this.updatedAt = project.updatedAt; + + this.trailerStatus = project.trailer?.status; + if (project.trailer && this.trailerStatus === 'completed') { + this.trailer = getPublicS3Url( + getTrailerTranscodedFileRoute(project.trailer.file), + ); + } else if (project.trailer) { + this.trailer = project.trailer?.file; + } + + if (project.poster) { + this.poster = getPublicS3Url(project.poster); + } + + this.screenings = project.screenings + ? project.screenings.map((s) => new ScreeningDTO(s)) + : []; + + this.awards = project.awards + ? project.awards.map((award) => new AwardDTO(award)) + : []; + + this.subgenres = project.projectSubgenres + ? project.projectSubgenres.map((ps) => new SubgenreDTO(ps.subgenre)) + : []; + } +} diff --git a/backend/src/projects/dto/response/document.dto.ts b/backend/src/projects/dto/response/document.dto.ts new file mode 100644 index 0000000..0fd03c7 --- /dev/null +++ b/backend/src/projects/dto/response/document.dto.ts @@ -0,0 +1,17 @@ +import { Document } from 'src/projects/entities/document.entity'; + +export class DocumentDTO { + id: string; + name: string; + url: string; + createdAt: Date; + updatedAt: Date; + + constructor(document: Document) { + this.id = document.id; + this.name = document.name; + this.url = document.url; + this.createdAt = document.createdAt; + this.updatedAt = document.updatedAt; + } +} diff --git a/backend/src/projects/dto/response/permission.dto.ts b/backend/src/projects/dto/response/permission.dto.ts new file mode 100644 index 0000000..ba96264 --- /dev/null +++ b/backend/src/projects/dto/response/permission.dto.ts @@ -0,0 +1,14 @@ +import { FilmmakerDTO } from 'src/filmmakers/dto/response/filmmaker.dto'; +import { Permission } from 'src/projects/entities/permission.entity'; +import { Role } from 'src/projects/enums/role.enum'; + +export class PermissionDTO extends FilmmakerDTO { + role: Role; + email?: string; + + constructor(permission: Permission) { + super(permission.filmmaker); + this.role = permission.role; + this.email = this.email ?? permission.email; + } +} diff --git a/backend/src/projects/dto/response/project.dto.ts b/backend/src/projects/dto/response/project.dto.ts new file mode 100644 index 0000000..92cc264 --- /dev/null +++ b/backend/src/projects/dto/response/project.dto.ts @@ -0,0 +1,54 @@ +import { DocumentDTO } from './document.dto'; +import { PermissionDTO } from './permission.dto'; +import { Project } from 'src/projects/entities/project.entity'; +import { BaseProjectDTO } from './base-project.dto'; +import { ContentDTO } from 'src/contents/dto/response/content.dto'; +import { Season } from 'src/season/entities/season.entity'; + +export class ProjectDTO extends BaseProjectDTO { + permissions: PermissionDTO[]; + documents: DocumentDTO[]; + processingContentIds?: string[]; + failedContentIds?: string[]; + publishedToRss: boolean; + rejectedReason?: string; + seasons: Season[]; + + constructor(project: Project) { + super(project); + + this.rejectedReason = project.rejectedReason; + + this.permissions = project.permissions + ? project.permissions.map((permission) => new PermissionDTO(permission)) + : []; + + this.documents = project.documents + ? project.documents.map((d) => new DocumentDTO(d)) + : []; + + if (this.type === 'episodic') { + this.episodes = project?.contents?.length + ? project.contents.map((content) => new ContentDTO(content)) + : []; + this.seasons = project?.seasons?.length ? project.seasons : []; + } else { + this.film = project?.contents?.length + ? new ContentDTO(project.contents[0]) + : undefined; + } + + if (project.contents) { + this.processingContentIds = project.contents + .filter((content) => content.status === 'processing' && content.file) + .map((content) => content.id); + this.failedContentIds = project.contents + .filter((content) => content.status === 'failed' && content.file) + .map((content) => content.id); + + if (project.contents.some((content) => content.isRssEnabled)) { + this.publishedToRss = true; + } + } + } +} diff --git a/backend/src/projects/dto/response/screening.dto.ts b/backend/src/projects/dto/response/screening.dto.ts new file mode 100644 index 0000000..e1d1388 --- /dev/null +++ b/backend/src/projects/dto/response/screening.dto.ts @@ -0,0 +1,18 @@ +import { FestivalDTO } from 'src/festivals/dto/response/festival.dto'; +import { FestivalScreening } from 'src/projects/entities/festival-screening.entity'; + +export class ScreeningDTO { + id: string; + festival: FestivalDTO; + year: number; + createdAt: Date; + updatedAt: Date; + + constructor(screening: FestivalScreening) { + this.id = screening.id; + this.year = screening.year; + this.festival = screening.festival; + this.createdAt = screening.createdAt; + this.updatedAt = screening.updatedAt; + } +} diff --git a/backend/src/projects/entities/award.entity.ts b/backend/src/projects/entities/award.entity.ts new file mode 100644 index 0000000..1c5582a --- /dev/null +++ b/backend/src/projects/entities/award.entity.ts @@ -0,0 +1,43 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Project } from './project.entity'; +import { AwardIssuer } from 'src/award-issuers/entities/award-issuer.entity'; + +@Entity('awards') +export class Award { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + projectId: string; + + @Column() + awardIssuerId: string; + + @Column() + name: string; + + @Column() + year: number; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Project, (project) => project.awards) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @ManyToOne(() => AwardIssuer, (ai) => ai.awards, { eager: true }) + @JoinColumn({ name: 'award_issuer_id' }) + issuer: AwardIssuer; +} diff --git a/backend/src/projects/entities/document.entity.ts b/backend/src/projects/entities/document.entity.ts new file mode 100644 index 0000000..ff0902a --- /dev/null +++ b/backend/src/projects/entities/document.entity.ts @@ -0,0 +1,35 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Project } from './project.entity'; + +@Entity('documents') +export class Document { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + projectId: string; + + @Column() + name: string; + + @Column() + url: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Project, (project) => project.documents) + @JoinColumn({ name: 'project_id' }) + project: Project; +} diff --git a/backend/src/projects/entities/festival-screening.entity.ts b/backend/src/projects/entities/festival-screening.entity.ts new file mode 100644 index 0000000..e0b7587 --- /dev/null +++ b/backend/src/projects/entities/festival-screening.entity.ts @@ -0,0 +1,40 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Project } from './project.entity'; +import { Festival } from 'src/festivals/entities/festival.entity'; + +@Entity('festival_screenings') +export class FestivalScreening { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + projectId: string; + + @Column() + festivalId: string; + + @Column() + year: number; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Project, (project) => project.screenings) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @ManyToOne(() => Festival, (festival) => festival.screenings) + @JoinColumn({ name: 'festival_id' }) + festival: Festival; +} diff --git a/backend/src/projects/entities/permission.entity.ts b/backend/src/projects/entities/permission.entity.ts new file mode 100644 index 0000000..f050612 --- /dev/null +++ b/backend/src/projects/entities/permission.entity.ts @@ -0,0 +1,38 @@ +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + Unique, +} from 'typeorm'; +import { Role } from '../enums/role.enum'; +import { Project } from './project.entity'; + +@Entity('permissions') +export class Permission { + @PrimaryColumn() + @Unique('id', ['filmmakerId', 'projectId']) + id: string; + + @Column({ nullable: true }) + filmmakerId?: string; + + @PrimaryColumn() + projectId: string; + + @Column({ nullable: true }) + email?: string; + + @Column() + role: Role; + + @ManyToOne(() => Filmmaker, (filmmaker) => filmmaker.films) + @JoinColumn({ name: 'filmmaker_id' }) + filmmaker: Filmmaker; + + @ManyToOne(() => Project, (project) => project.permissions) + @JoinColumn({ name: 'project_id' }) + project: Project; +} diff --git a/backend/src/projects/entities/project-genre.entity.ts b/backend/src/projects/entities/project-genre.entity.ts new file mode 100644 index 0000000..b9c5866 --- /dev/null +++ b/backend/src/projects/entities/project-genre.entity.ts @@ -0,0 +1,41 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Project } from './project.entity'; +import { Subgenre } from 'src/genres/entities/subgenre.entity'; + +@Entity('project_subgenres') +export class ProjectGenre { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + projectId: string; + + // TODO: Remove this after subgenre migration + @Column({ nullable: true }) + genreId: string; + + @Column({ nullable: true }) + subgenreId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Project, (project) => project.projectSubgenres) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @ManyToOne(() => Subgenre, (subgenre) => subgenre.projects) + @JoinColumn({ name: 'subgenre_id' }) + subgenre: Subgenre; +} diff --git a/backend/src/projects/entities/project.entity.ts b/backend/src/projects/entities/project.entity.ts new file mode 100644 index 0000000..b21ef91 --- /dev/null +++ b/backend/src/projects/entities/project.entity.ts @@ -0,0 +1,201 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; +import { Status } from '../enums/status.enum'; +import { Award } from './award.entity'; +import { FestivalScreening } from './festival-screening.entity'; +import { Document } from './document.entity'; +import { ProjectGenre } from './project-genre.entity'; +import { Permission } from './permission.entity'; +import { Format } from '../enums/format.enum'; +import { Content } from 'src/contents/entities/content.entity'; +import { Category } from '../enums/category.enum'; +import { Type } from '../enums/type.enum'; +import { ColumnNumericTransformer } from 'src/database/transformers/column-numeric-transformer'; +import { Genre } from 'src/genres/entities/genre.entity'; +import { Season } from 'src/season/entities/season.entity'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; +import { LibraryItem } from 'src/library/entities/library-item.entity'; + +@Entity('projects') +export class Project { + @PrimaryColumn() + id: string; + + @Column({ default: '' }) + name: string; + + @Column() + title: string; + + @Column({ nullable: true }) + @Unique('slug', ['slug']) + slug: string; + + @Column({ nullable: true }) + synopsis: string; + + @Column({ nullable: true }) + trailer_old: string; + + @ManyToOne(() => Trailer, (trailer) => trailer.projects, { + nullable: true, + cascade: true, + }) + @JoinColumn({ name: 'trailer_id' }) + trailer?: Trailer; + + @Column({ nullable: true }) + poster: string; + + @Column({ default: 'draft' }) + status: Status; + + @Column({ default: 'film' }) + type: Type; + + @Column({ nullable: true }) + format: Format; + + @Column({ nullable: true }) + category: Category; + + @Column('decimal', { + precision: 10, + scale: 2, + default: 0, + transformer: new ColumnNumericTransformer(), + }) + rentalPrice: number; + + @Column({ nullable: true }) + rejectedReason: string; + + @Column({ nullable: true }) + genreId: string; + + // ── Dual-mode content delivery ────────────────────────────────── + // 'native' = stored in MinIO, transcoded by our FFmpeg worker, AES-128 encrypted + // 'partner' = served from partner CDN (e.g., IndeeHub's AWS infrastructure) + @Column({ default: 'native' }) + deliveryMode: 'native' | 'partner'; + + // Partner-mode streaming URLs (only populated when deliveryMode = 'partner') + @Column({ nullable: true }) + partnerStreamUrl: string; + + @Column({ nullable: true }) + partnerDashUrl: string; + + @Column({ nullable: true }) + partnerFairplayUrl: string; + + @Column({ nullable: true }) + partnerDrmToken: string; + + @Column({ nullable: true }) + partnerApiBaseUrl: string; + + // Native-mode streaming URL (HLS manifest path in MinIO) + @Column({ nullable: true }) + streamingUrl: string; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; + + @OneToMany(() => Permission, (permission) => permission.project) + permissions: Permission[]; + + @OneToMany(() => Award, (award) => award.project) + awards: Award[]; + + @OneToMany(() => FestivalScreening, (screening) => screening.project) + screenings: FestivalScreening[]; + + @OneToMany(() => Document, (document) => document.project) + documents: Document[]; + + @OneToMany(() => ProjectGenre, (projectGenre) => projectGenre.project) + projectSubgenres: ProjectGenre[]; + + @OneToMany(() => Content, (content) => content.project) + contents: Content[]; + + @ManyToOne(() => Genre, (genre) => genre.projects) + genre: Genre; + + @OneToMany(() => Season, (season) => season.project) + seasons: Season[]; + + @OneToMany(() => LibraryItem, (item) => item.project) + libraryItems: LibraryItem[]; +} + +export const defaultRelations = [ + 'contents', + 'contents.shareholders', + 'contents.shareholders.filmmaker', + 'contents.shareholders.filmmaker.user', + 'contents.cast', + 'contents.cast.filmmaker', + 'contents.cast.filmmaker.user', + 'contents.crew', + 'contents.crew.filmmaker', + 'contents.crew.filmmaker.user', + 'contents.invites', + 'contents.trailer', + 'screenings', + 'screenings.festival', + 'awards', + 'awards.issuer', + 'genre', + 'projectSubgenres', + 'projectSubgenres.subgenre', + 'seasons', + 'trailer', +]; + +export const fullRelations = [ + 'permissions', + 'permissions.filmmaker', + 'permissions.filmmaker.user', + 'contents', + 'contents.captions', + 'contents.shareholders', + 'contents.shareholders.filmmaker', + 'contents.shareholders.filmmaker.user', + 'contents.rssShareholders', + 'contents.cast', + 'contents.cast.filmmaker', + 'contents.cast.filmmaker.user', + 'contents.crew', + 'contents.crew.filmmaker', + 'contents.crew.filmmaker.user', + 'contents.invites', + 'contents.trailer', + 'documents', + 'screenings', + 'screenings.festival', + 'awards', + 'awards.issuer', + 'genre', + 'projectSubgenres', + 'projectSubgenres.subgenre', + 'seasons', + 'trailer', +]; diff --git a/backend/src/projects/enums/category.enum.ts b/backend/src/projects/enums/category.enum.ts new file mode 100644 index 0000000..22a6c4c --- /dev/null +++ b/backend/src/projects/enums/category.enum.ts @@ -0,0 +1,2 @@ +export const categories = ['narrative', 'documentary'] as const; +export type Category = (typeof categories)[number]; diff --git a/backend/src/projects/enums/format.enum.ts b/backend/src/projects/enums/format.enum.ts new file mode 100644 index 0000000..127df2d --- /dev/null +++ b/backend/src/projects/enums/format.enum.ts @@ -0,0 +1,2 @@ +export const formats = ['short', 'feature'] as const; +export type Format = (typeof formats)[number]; diff --git a/backend/src/projects/enums/role.enum.ts b/backend/src/projects/enums/role.enum.ts new file mode 100644 index 0000000..8e36d43 --- /dev/null +++ b/backend/src/projects/enums/role.enum.ts @@ -0,0 +1,11 @@ +export const roles = [ + 'owner', + 'admin', + 'editor', + 'viewer', + 'revenue-manager', +] as const; +export type Role = (typeof roles)[number]; + +export const memberTypes = [...roles, 'shareholder', 'cast-crew'] as const; +export type MemberType = (typeof memberTypes)[number]; diff --git a/backend/src/projects/enums/status.enum.ts b/backend/src/projects/enums/status.enum.ts new file mode 100644 index 0000000..63ef0e7 --- /dev/null +++ b/backend/src/projects/enums/status.enum.ts @@ -0,0 +1,7 @@ +export const statuses = [ + 'rejected', + 'draft', + 'under-review', + 'published', +] as const; +export type Status = (typeof statuses)[number]; diff --git a/backend/src/projects/enums/type.enum.ts b/backend/src/projects/enums/type.enum.ts new file mode 100644 index 0000000..9de0574 --- /dev/null +++ b/backend/src/projects/enums/type.enum.ts @@ -0,0 +1,2 @@ +export const types = ['episodic', 'film', 'music-video'] as const; +export type Type = (typeof types)[number]; diff --git a/backend/src/projects/guards/permission.guard.ts b/backend/src/projects/guards/permission.guard.ts new file mode 100644 index 0000000..e82a41c --- /dev/null +++ b/backend/src/projects/guards/permission.guard.ts @@ -0,0 +1,73 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Observable } from 'rxjs'; +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { Roles } from '../decorators/roles.decorator'; +import { MemberType } from '../enums/role.enum'; +import { ProjectsService } from '../projects.service'; + +@Injectable() +export class PermissionGuard implements CanActivate { + constructor( + @Inject(ProjectsService) + private projectsService: ProjectsService, + private reflector: Reflector, + ) {} + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const roles = this.reflector.get(Roles, context.getHandler()); + + const request = context.switchToHttp().getRequest(); + const projectId = request.params.id; + const user = request.user; + + return projectId + ? this.validateIdentity(projectId, user.filmmaker as Filmmaker, roles) + : user.filmmaker.id; + } + + async validateIdentity( + projectId: string, + filmmaker: Filmmaker, + roles: MemberType[], + ) { + const project = await this.projectsService.findOne(projectId, [ + 'permissions', + 'contents', + 'contents.shareholders', + 'contents.cast', + 'contents.crew', + ]); + const filmUser = project.permissions.find( + (f) => f.filmmakerId === filmmaker.id && roles.includes(f.role), + ); + if ( + roles.includes('shareholder') && + project.contents.some((content) => + content.shareholders.find((f) => f.id === filmmaker.id), + ) + ) { + return true; + } + if ( + roles.includes('cast-crew') && + project.contents.some( + (content) => + content.cast.find((f) => f.id === filmmaker.id) || + content.crew.find((f) => f.id === filmmaker.id), + ) + ) { + return true; + } + if (!filmUser) { + return false; + } + return true; + } +} diff --git a/backend/src/projects/interceptor/project-cache.interceptor.ts b/backend/src/projects/interceptor/project-cache.interceptor.ts new file mode 100644 index 0000000..9a31f93 --- /dev/null +++ b/backend/src/projects/interceptor/project-cache.interceptor.ts @@ -0,0 +1,26 @@ +import { CacheInterceptor } from '@nestjs/cache-manager'; +import { ExecutionContext, Injectable } from '@nestjs/common'; + +@Injectable() +export class ProjectsCacheInterceptor extends CacheInterceptor { + trackBy(context: ExecutionContext): string | undefined { + const request = context.switchToHttp().getRequest(); + const url = request.url; + return this.getKey(url); + } + + getKey(url: string) { + const temporalUrl = new URL('https://temporal.com' + url); + const parameters = temporalUrl.searchParams; + + const originalSeed = parameters.get('randomSeed'); + if (originalSeed) { + // Preserving randomSeed will prevent to hit the cache + parameters.set('randomSeed', 'true'); + } + + const key = temporalUrl.pathname + '?' + parameters.toString(); + + return key; + } +} diff --git a/backend/src/projects/interceptor/projects.interceptor.ts b/backend/src/projects/interceptor/projects.interceptor.ts new file mode 100644 index 0000000..c1b57ba --- /dev/null +++ b/backend/src/projects/interceptor/projects.interceptor.ts @@ -0,0 +1,89 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, map } from 'rxjs'; +import { UploadService } from 'src/upload/upload.service'; +import { ProjectDTO } from '../dto/response/project.dto'; + +@Injectable() +export class ProjectsInterceptor implements NestInterceptor { + constructor(private readonly uploadService: UploadService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next + .handle() + .pipe( + map(async (data) => + Array.isArray(data) + ? Promise.all(data.map((project) => this.convertProject(project))) + : this.convertProject(data), + ), + ); + } + + async convertProject(project: ProjectDTO) { + const modifiedProject = { ...project }; + + if (modifiedProject.documents) { + modifiedProject.documents = await Promise.all( + modifiedProject.documents.map(async (document) => { + const modifiedDocument = { ...document }; + if (modifiedDocument.url) { + modifiedDocument.url = await this.uploadService.createPresignedUrl({ + key: modifiedDocument.url, + }); + } + return modifiedDocument; + }), + ); + } + + if (modifiedProject.film?.file) { + modifiedProject.film = { ...modifiedProject.film }; + modifiedProject.film.file = await this.uploadService.createPresignedUrl({ + key: modifiedProject.film.file, + }); + } + + if ( + !modifiedProject.trailer?.startsWith('http') && + modifiedProject.trailer + ) { + modifiedProject.trailer = await this.uploadService.createPresignedUrl({ + key: modifiedProject.trailer, + expires: 60 * 60 * 24 * 7, + }); + } + + if (modifiedProject.episodes) { + modifiedProject.episodes = await Promise.all( + modifiedProject.episodes.map(async (episode) => { + const modifiedEpisode = { ...episode }; + if (modifiedEpisode.file) { + modifiedEpisode.file = await this.uploadService.createPresignedUrl({ + key: modifiedEpisode.file, + }); + } + if ( + !modifiedEpisode.trailer?.startsWith('http') && + modifiedEpisode.trailer + ) { + modifiedEpisode.trailer = + await this.uploadService.createPresignedUrl({ + key: modifiedEpisode.trailer, + expires: 60 * 60 * 24 * 7, + }); + } + return modifiedEpisode; + }), + ); + } + + Logger.log({ modifiedProject }, 'ProjectsInterceptor'); + return modifiedProject; + } +} diff --git a/backend/src/projects/projects.controller.ts b/backend/src/projects/projects.controller.ts new file mode 100644 index 0000000..f6e566a --- /dev/null +++ b/backend/src/projects/projects.controller.ts @@ -0,0 +1,242 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, + Query, + UseInterceptors, +} from '@nestjs/common'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { ProjectsInterceptor } from './interceptor/projects.interceptor'; +import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator'; +import { Roles } from './decorators/roles.decorator'; +import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard'; +import { PermissionGuard } from './guards/permission.guard'; +import { ProjectsService } from './projects.service'; +import { CreateProjectDTO } from './dto/request/create-project.dto'; +import { ProjectDTO } from './dto/response/project.dto'; +import { ListProjectsDTO } from './dto/request/list-projects.dto'; +import { UpdateProjectDTO } from './dto/request/update-project.dto'; +import { fullRelations } from './entities/project.entity'; +import { BaseProjectDTO } from './dto/response/base-project.dto'; +import { AdminValidationDTO } from './dto/request/admin-validation.dto'; +import { AdminAuthGuard } from 'src/auth/guards/admin.guard'; +import { ProjectsRevenueDTO } from './dto/request/revenue-projects.dto'; +import { CacheTTL } from '@nestjs/cache-manager'; +import { ProjectsCacheInterceptor } from './interceptor/project-cache.interceptor'; +import { TokenAuthGuard } from 'src/auth/guards/token.guard'; +import { TranscodingCompletedDTO } from 'src/contents/dto/request/transcoding-completed.dto'; + +@Controller('projects') +export class ProjectsController { + constructor(private readonly projectsService: ProjectsService) {} + + @Post() + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + async create( + @Body() createProjectDTO: CreateProjectDTO, + @User() user: RequestUser['user'], + ) { + const project = await this.projectsService.create( + createProjectDTO, + user.filmmaker.id, + ); + return new ProjectDTO(project); + } + + @Get() + @CacheTTL(1000 * 60 * 5) + @UseInterceptors(ProjectsCacheInterceptor) + async findAll(@Query() query: ListProjectsDTO) { + const projects = await this.projectsService.findAll({ + ...query, + status: 'published', + }); + return projects.map((project) => new BaseProjectDTO(project)); + } + + @Patch('admin/:id') + @UseGuards(AdminAuthGuard) + async approvalFlow( + @Param('id') id: string, + @Body() { status, rejectedReason, permissions }: AdminValidationDTO, + ) { + const project = await this.projectsService.adminValidation( + id, + status, + rejectedReason, + permissions, + ); + return new ProjectDTO(project); + } + + @Get('/count') + @CacheTTL(1000 * 60 * 5) + @UseInterceptors(ProjectsCacheInterceptor) + async findAllCount(@Query() query: ListProjectsDTO) { + const total = await this.projectsService.getCount({ + ...query, + status: 'published', + }); + return { count: total }; + } + + @Get('/private') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + async findAllPrivate( + @Query() query: ListProjectsDTO, + @User() user: RequestUser['user'], + ) { + const projects = await this.projectsService.findAll({ + ...query, + filmmakerId: user.filmmaker.id, + relations: true, + }); + return projects.map((project) => new ProjectDTO(project)); + } + + @Get('/private/count') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + async findAllPrivateCount( + @Query() query: ListProjectsDTO, + @User() user: RequestUser['user'], + ) { + const total = await this.projectsService.getCount({ + ...query, + filmmakerId: user.filmmaker.id, + }); + return { count: total }; + } + + @Get('/private/:id') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @UseInterceptors(ProjectsInterceptor) + @Subscriptions(['filmmaker']) + @Roles([ + 'owner', + 'admin', + 'editor', + 'revenue-manager', + 'cast-crew', + 'shareholder', + 'viewer', + ]) + async findOnePrivate(@Param('id') id: string) { + const project = await this.projectsService.findOne(id, fullRelations); + return new ProjectDTO(project); + } + + @Get('/revenue') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + @Roles(['owner', 'shareholder']) + async getRevenue( + @User() user: RequestUser['user'], + @Query() { ids, dateRange }: ProjectsRevenueDTO, + ) { + return this.projectsService.getProjectsRevenue( + ids, + user.filmmaker.id, + dateRange, + ); + } + + @Get('admin/private/:id') + @UseGuards(AdminAuthGuard) + @UseInterceptors(ProjectsInterceptor) + async findOnePrivateProject(@Param('id') id: string) { + const project = await this.projectsService.findOne(id, fullRelations); + return new ProjectDTO(project); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const project = await this.projectsService.findOne(id); + return new BaseProjectDTO(project); + } + + @Patch(':id') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @UseInterceptors(ProjectsInterceptor) + @Subscriptions(['filmmaker']) + @Roles(['owner', 'admin', 'editor', 'revenue-manager']) + async update( + @Param('id') id: string, + @Body() updateProjectDTO: UpdateProjectDTO, + @User() user: RequestUser['user'], + ) { + const project = await this.projectsService.update( + id, + updateProjectDTO, + user.subscriptions.some((sub) => { + if (sub.type === 'rss-addon') { + const currentDate = new Date(); + // get subscription expiration date + const subscriptionExpirationDate = sub.periodEnd; + // check if subscription is active + return currentDate < subscriptionExpirationDate; + } + return false; + }), + ); + return new ProjectDTO(project); + } + + @Delete(':id') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + @Roles(['owner']) + remove(@Param('id') id: string) { + return this.projectsService.remove(id); + } + + @Post(':id/trailer/transcoding') + @UseGuards(TokenAuthGuard) + async trailerTranscoding( + @Param('id') id: string, + @Body() body: TranscodingCompletedDTO, + ) { + await this.projectsService.trailerTranscodingCompleted( + id, + body.status, + body.metadata, + ); + } + + @Post(':id/invite') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + @Roles(['owner', 'admin', 'editor', 'revenue-manager']) + async inviteShareholder( + @Param('id') id: string, + @Body('email') email: string, + ) { + return this.projectsService.resendInvite(id, email); + } + + // Can this handler be removed ??? + @Get(':id/stream') + @UseGuards(HybridAuthGuard, SubscriptionsGuard) + @Subscriptions(['enthusiast', 'film-buff', 'cinephile']) + @UseInterceptors(ProjectsInterceptor) + async stream(@Param('id') id: string) { + return new ProjectDTO(await this.projectsService.stream(id)); + } + + @Get(':id/slug/:slug/exists') + @UseGuards(HybridAuthGuard, SubscriptionsGuard, PermissionGuard) + @Subscriptions(['filmmaker']) + @Roles(['owner', 'admin', 'editor']) + async checkSlug(@Param('id') id: string, @Param('slug') slug: string) { + return this.projectsService.checkIfSlugExists(id, slug); + } +} diff --git a/backend/src/projects/projects.module.ts b/backend/src/projects/projects.module.ts new file mode 100644 index 0000000..bd2094e --- /dev/null +++ b/backend/src/projects/projects.module.ts @@ -0,0 +1,44 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { ProjectsController } from './projects.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Project } from './entities/project.entity'; +import { InvitesModule } from 'src/invites/invites.module'; +import { Award } from './entities/award.entity'; +import { FestivalScreening } from './entities/festival-screening.entity'; +import { Document } from './entities/document.entity'; +import { Permission } from './entities/permission.entity'; +import { ProjectsService } from './projects.service'; +import { ProjectGenre } from './entities/project-genre.entity'; +import { ContentsModule } from 'src/contents/contents.module'; +import { ConfigModule } from '@nestjs/config'; +import { Season } from 'src/season/entities/season.entity'; +import { SeasonRent } from 'src/season/entities/season-rents.entity'; +import { SeasonService } from 'src/season/season.service'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; +import { BullModule } from '@nestjs/bullmq'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Project, + Permission, + Award, + FestivalScreening, + Document, + ProjectGenre, + Season, + SeasonRent, + Trailer, + ]), + InvitesModule, + forwardRef(() => ContentsModule), + ConfigModule, + BullModule.registerQueue({ + name: 'transcode', + }), + ], + controllers: [ProjectsController], + providers: [ProjectsService, SeasonService], + exports: [ProjectsService], +}) +export class ProjectsModule {} diff --git a/backend/src/projects/projects.service.ts b/backend/src/projects/projects.service.ts new file mode 100644 index 0000000..a8952f7 --- /dev/null +++ b/backend/src/projects/projects.service.ts @@ -0,0 +1,1185 @@ +import { + forwardRef, + Inject, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectQueue } from '@nestjs/bullmq'; +import { CreateProjectDTO } from './dto/request/create-project.dto'; +import { + AddPermissionDTO, + UpdateProjectDTO, +} from './dto/request/update-project.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Brackets, In, Not, Repository } from 'typeorm'; +import { + Project, + defaultRelations, + fullRelations, +} from './entities/project.entity'; +import { randomUUID } from 'node:crypto'; +import { ListProjectsDTO } from './dto/request/list-projects.dto'; +import { UploadService } from 'src/upload/upload.service'; +import { MailService } from 'src/mail/mail.service'; +import { Document } from './entities/document.entity'; +import { Award } from './entities/award.entity'; +import { FestivalScreening } from './entities/festival-screening.entity'; +import { ProjectGenre } from './entities/project-genre.entity'; +import { Permission } from './entities/permission.entity'; +import { ContentsService } from 'src/contents/contents.service'; +import axios from 'axios'; +import { + getFilterDateRange, + getTrailerTranscodedFileRoute, + getTrailerTranscodeOutputKey, + isProjectRSSReady, +} from 'src/common/helper'; +import { Status } from './enums/status.enum'; +import { FilterDateRange } from 'src/payment/enums/filter-date-range.enum'; +import { ConfigService } from '@nestjs/config'; +import { SeasonService } from 'src/season/season.service'; +import { UpdateSeasonDto } from 'src/season/dto/request/update-season.dto.entity'; +import { Queue } from 'bullmq'; +import { Transcode } from 'src/contents/types/transcode'; +import { ContentStatus } from 'src/contents/enums/content-status.enum'; +import { Trailer } from 'src/trailers/entities/trailer.entity'; + +@Injectable() +export class ProjectsService { + constructor( + @InjectRepository(Project) + private projectsRepository: Repository, + @InjectRepository(Permission) + private permissionRepository: Repository, + @InjectRepository(Document) + private documentRepository: Repository, + @InjectRepository(Award) + private awardRepository: Repository, + @InjectRepository(FestivalScreening) + private festivalRepository: Repository, + @InjectRepository(ProjectGenre) + private projectGenreRepository: Repository, + @InjectRepository(Trailer) + private trailerRepository: Repository, + @Inject(UploadService) + private uploadService: UploadService, + @Inject(MailService) + private mailService: MailService, + @Inject(forwardRef(() => ContentsService)) + private contentsService: ContentsService, + @Inject(ConfigService) + private readonly configService: ConfigService, + @Inject(SeasonService) + private readonly seasonService: SeasonService, + @InjectQueue('transcode') private transcodeQueue: Queue, + ) {} + + async create(createProjectDTO: CreateProjectDTO, filmmakerId: string) { + const projectId = randomUUID(); + const project = await this.projectsRepository.save({ + id: projectId, + ...createProjectDTO, + title: createProjectDTO.name, + name: createProjectDTO.name, + slug: projectId, + }); + + const isEpisodic = project.type === 'episodic'; + + if (isEpisodic) { + const existingSeason = + await this.seasonService.findSeasonsByProjectAndNumbers(project.id, [ + 1, + ]); + + if (existingSeason.length === 0) { + // Create Season + await this.seasonService.getOrCreateSeasonsUpsert(project.id, [ + { seasonNumber: 1 } as UpdateSeasonDto, + ]); + } + } + + await this.contentsService.upsert(project.id, { + title: isEpisodic ? 'Episode 1' : createProjectDTO.name, + season: isEpisodic ? 1 : undefined, + shareholders: [ + { + id: filmmakerId, + share: 100, + }, + ], + }); + + await this.permissionRepository.save({ + id: randomUUID(), + projectId: project.id, + filmmakerId, + role: 'owner', + }); + + return this.findOne(project.id); + } + + async findAll(query: ListProjectsDTO) { + const projectsQuery = this.getProjectsQuery(query); + projectsQuery.offset(query.offset); + projectsQuery.limit(query.limit); + const projects = await projectsQuery.getMany(); + + if (query.relations) { + const projectIds = projects.map((project) => project.id); + const filmmakers = await this.permissionRepository.find({ + where: { projectId: In(projectIds) }, + relations: ['filmmaker'], + }); + return projects.map((project) => { + project.permissions = filmmakers.filter( + (permission) => permission.projectId === project.id, + ); + return project; + }); + } else { + return projects; + } + } + + async getCount(query: ListProjectsDTO) { + const projectsQuery = this.getProjectsQuery(query); + return projectsQuery.getCount(); + } + + getProjectsQuery(query: ListProjectsDTO) { + const projectsQuery = this.projectsRepository.createQueryBuilder('project'); + projectsQuery.distinct(true); + projectsQuery.leftJoin('project.contents', 'contents'); + + if (query.relations) { + projectsQuery.addSelect([ + 'contents.id', + 'contents.status', + 'contents.isRssEnabled', + ]); + } + + if (query.status) { + if (query.status === 'published') { + const completed = this.contentsService.getCompletedProjectsSubquery(); + projectsQuery.andWhere('project.id IN (' + completed.getQuery() + ')'); + projectsQuery.setParameters(completed.getParameters()); + } + projectsQuery.andWhere('project.status = :status', { + status: query.status, + }); + } + + if (query.search) { + projectsQuery.leftJoin('project.projectSubgenres', 'projectSubgenres'); + projectsQuery.leftJoin('projectSubgenres.subgenre', 'subgenre'); + projectsQuery.leftJoin('project.genre', 'genre'); + + projectsQuery.leftJoin('contents.cast', 'castMembers'); + projectsQuery.leftJoin('castMembers.filmmaker', 'castFilmmaker'); + projectsQuery.leftJoin('contents.crew', 'crewMembers'); + projectsQuery.leftJoin('crewMembers.filmmaker', 'crewFilmmaker'); + + projectsQuery.addSelect(` + CASE + WHEN project.title ILIKE :name THEN 1 + WHEN genre.name ILIKE :name THEN 2 + WHEN subgenre.name ILIKE :name THEN 2 + WHEN castFilmmaker.professional_name ILIKE :name THEN 3 + WHEN crewFilmmaker.professional_name ILIKE :name THEN 3 + ELSE 4 + END as search + `); + + projectsQuery.andWhere( + new Brackets((qb) => { + qb.where('project.title ILIKE :name', { + name: `%${query.search}%`, + }) + .orWhere('subgenre.name ILIKE :name', { + name: `%${query.search}%`, + }) + .orWhere('genre.name ILIKE :name', { + name: `%${query.search}%`, + }) + .orWhere('castFilmmaker.professional_name ILIKE :name', { + name: `%${query.search}%`, + }) + .orWhere('crewFilmmaker.professional_name ILIKE :name', { + name: `%${query.search}%`, + }); + }), + ); + } + + if (query.awards) { + projectsQuery.leftJoin('project.awards', 'awards'); + projectsQuery.andWhere('awards.id IS NOT NULL'); + } + + if (query.screenings) { + projectsQuery.leftJoin('project.screenings', 'screenings'); + projectsQuery.andWhere('screenings.id IS NOT NULL'); + } + + if ( + query.genres && + query.genres.ids?.length > 0 && + !query.subgenres?.ids?.length + ) { + projectsQuery.leftJoin('project.projectSubgenres', 'projectSubgenres'); + projectsQuery.leftJoin('projectSubgenres.subgenre', 'subgenre'); + if (query.genres.all) { + projectsQuery.andWhere('subgenre.genreId IN (:...ids)', { + ids: query.genres.ids, + }); + } else { + projectsQuery.andWhere('project.genreId = ANY(:ids)', { + ids: query.genres.ids, + }); + } + } else if (query.subgenres && query.subgenres.ids?.length > 0) { + projectsQuery.leftJoin('project.projectSubgenres', 'projectSubgenres'); + if (query.subgenres.all) { + projectsQuery.andWhere('projectSubgenres.subgenreId IN (:...ids)', { + ids: query.subgenres.ids, + }); + } else { + projectsQuery.andWhere('projectSubgenres.subgenreId = ANY(:ids)', { + ids: query.subgenres.ids, + }); + } + } + + if (query.festivals && query.festivals.length > 0) { + projectsQuery.leftJoin('project.screenings', 'screenings'); + projectsQuery.andWhere('screenings.festivalId IN (:...festivals)', { + festivals: query.festivals, + }); + } + + if (query.format) { + projectsQuery.andWhere('project.format = :format', { + format: query.format, + }); + } + + if (query.type) { + projectsQuery.andWhere('project.type = :type', { type: query.type }); + } + + if (query.filmmakerId) { + const subQueryCast = this.contentsService.getCastFilterSubquery( + query.filmmakerId, + ); + const subQueryCrew = this.contentsService.getCrewFilterSubquery( + query.filmmakerId, + ); + + const subQueryShareholder = + this.contentsService.getShareholderFilterSubquery(query.filmmakerId); + + const subQueryPermission = this.permissionRepository + .createQueryBuilder('permissions') + .select('permissions.projectId') + .where('permissions.filmmakerId = :filmmakerId', { + filmmakerId: query.filmmakerId, + }); + + if (query.show === 'all') { + projectsQuery + .andWhere( + new Brackets((qb) => { + qb.where('project.id IN (' + subQueryCast.getQuery() + ')') + .orWhere('project.id IN (' + subQueryCrew.getQuery() + ')') + .orWhere( + 'project.id IN (' + subQueryShareholder.getQuery() + ')', + ) + .orWhere( + 'project.id IN (' + subQueryPermission.getQuery() + ')', + ); + }), + ) + .setParameters(subQueryCast.getParameters()) + .setParameters(subQueryCrew.getParameters()) + .setParameters(subQueryShareholder.getParameters()) + .setParameters(subQueryPermission.getParameters()); + } + if (query.show === 'cast-crew') { + projectsQuery + .andWhere( + new Brackets((qb) => { + qb.where( + 'project.id IN (' + subQueryCast.getQuery() + ')', + ).orWhere('project.id IN (' + subQueryCrew.getQuery() + ')'); + }), + ) + .setParameters(subQueryCast.getParameters()) + .setParameters(subQueryCrew.getParameters()); + } + + if (query.show === 'shareholder') { + projectsQuery + .andWhere('project.id IN (' + subQueryShareholder.getQuery() + ')') + .setParameters(subQueryShareholder.getParameters()); + } + + if (query.show === 'owner') { + projectsQuery + .andWhere('project.id IN (' + subQueryPermission.getQuery() + ')') + .setParameters(subQueryPermission.getParameters()); + } + } + if (query.category) { + projectsQuery.andWhere('project.category = :category', { + category: query.category, + }); + } + + if (query.sort) { + if (query.sort === 'random' && query.randomSeed) { + projectsQuery + .addSelect( + `MOD(TO_CHAR(project.updated_at, 'HH12MMSS')::int, ${query.randomSeed})`, + `random`, + ) + .orderBy('random', 'ASC'); + } else { + projectsQuery.addOrderBy('project.' + query.sort, query.order); + } + } else if (query.search) { + projectsQuery.addOrderBy('search', 'ASC'); + } else { + projectsQuery.addOrderBy('project.updated_at', 'DESC'); + } + + return projectsQuery; + } + + async findOne(id: string, relations: string[] = defaultRelations) { + try { + const film = await this.projectsRepository.findOneOrFail({ + where: [{ id }, { slug: id }], + relations, + order: { + contents: { + season: 'ASC', + order: 'ASC', + }, + }, + }); + return film; + } catch { + throw new NotFoundException('Film not found'); + } + } + + async findMany(ids: string[], relations: string[] = defaultRelations) { + try { + const film = await this.projectsRepository.find({ + where: [{ id: In(ids) }], + relations, + order: { + contents: { + season: 'ASC', + order: 'ASC', + }, + }, + }); + return film; + } catch { + throw new NotFoundException('Film not found'); + } + } + + async update( + id: string, + updateProjectDTO: UpdateProjectDTO, + hasActiveRssAddon: boolean, + ) { + const project = await this.findOne(id, fullRelations); + const previousProjectStatus = project.status; + const trailerProvided = Object.prototype.hasOwnProperty.call( + updateProjectDTO, + 'trailer', + ); + const existingTrailerFile = project.trailer?.file; + let shouldQueueProjectTrailer = false; + let updatedTrailer: Trailer | null | undefined; + let trailerToRemove: Trailer | undefined; + let removeTrailerJobs = false; + + if (trailerProvided) { + const newTrailerFile = updateProjectDTO.trailer; + if (newTrailerFile) { + const trailerChanged = existingTrailerFile !== newTrailerFile; + if (trailerChanged && existingTrailerFile) { + await this.deleteTrailerAssets(existingTrailerFile); + } + const trailerEntity = + project.trailer ?? this.trailerRepository.create(); + const resetTranscoding = + trailerChanged || project.trailer === undefined; + trailerEntity.file = newTrailerFile; + if (resetTranscoding) { + trailerEntity.status = 'processing'; + trailerEntity.metadata = undefined; + } + updatedTrailer = await this.trailerRepository.save(trailerEntity); + shouldQueueProjectTrailer = resetTranscoding; + } else if (project.trailer) { + if (existingTrailerFile) { + await this.deleteTrailerAssets(existingTrailerFile); + } + trailerToRemove = project.trailer; + // eslint-disable-next-line unicorn/no-null -- Explicitly clear the trailer relation. + updatedTrailer = null; + removeTrailerJobs = true; + } else { + updatedTrailer = undefined; + } + } + + const projectUpdatePayload: Partial = { + name: updateProjectDTO.name, + title: updateProjectDTO.title, + slug: updateProjectDTO.slug, + synopsis: updateProjectDTO.synopsis, + poster: updateProjectDTO.poster, + status: + updateProjectDTO.status === 'published' && + (project.status === 'draft' || project.status === 'rejected') + ? 'under-review' + : updateProjectDTO.status, + category: updateProjectDTO.category, + format: updateProjectDTO.format, + genreId: updateProjectDTO.genreId, + rejectedReason: '', + }; + + await this.projectsRepository.update(id, projectUpdatePayload); + + if (trailerProvided && updatedTrailer !== undefined) { + await this.projectsRepository + .createQueryBuilder() + .relation(Project, 'trailer') + .of(project.id) + .set(updatedTrailer); + project.trailer = updatedTrailer ?? undefined; + } + if (trailerToRemove) { + await this.trailerRepository.remove(trailerToRemove); + } + + const newMembers: Array<{ id?: string; email?: string }> = []; + const permissionPromises = + updateProjectDTO.permissions && updateProjectDTO.permissions.length > 0 + ? [ + ...project.permissions.map((permission) => { + const permissionDTO = updateProjectDTO.permissions.find( + (p) => + p.id === permission.filmmakerId || + (permission.email && p.email === permission.email), + ); + return permissionDTO + ? this.permissionRepository.update( + { + filmmakerId: permission.filmmakerId, + projectId: project.id, + }, + { + role: permissionDTO.role, + }, + ) + : this.permissionRepository.delete({ + filmmakerId: permission.filmmakerId, + projectId: project.id, + }); + }), + ...updateProjectDTO.permissions.map((permissionDTO) => { + const permission = project.permissions.find( + (p) => + p.filmmakerId === permissionDTO.id || + p.email === permissionDTO.email, + ); + if (!permission) { + newMembers.push(permissionDTO); + return this.permissionRepository.save({ + id: randomUUID(), + role: permissionDTO.role, + email: permissionDTO.email, + projectId: project.id, + filmmakerId: permissionDTO.id, + }); + } + return Promise.resolve(); + }), + ] + : []; + + const awardPromises = updateProjectDTO.awards + ? [ + ...project.awards.map((award) => { + const awardDTO = updateProjectDTO.awards.find( + (aw) => aw.id === award.id, + ); + return awardDTO + ? this.awardRepository.update( + { + id: award.id, + projectId: project.id, + }, + { + name: awardDTO.name, + year: awardDTO.year, + awardIssuerId: awardDTO.awardIssuerId, + }, + ) + : this.awardRepository.delete({ + id: award.id, + projectId: project.id, + }); + }), + ...updateProjectDTO.awards.map((awardDTO) => { + const award = project.awards.find((aw) => aw.id === awardDTO.id); + if (!award) { + return this.awardRepository.save({ + id: randomUUID(), + name: awardDTO.name, + year: awardDTO.year, + awardIssuerId: awardDTO.awardIssuerId, + projectId: project.id, + }); + } + return Promise.resolve(); + }), + ] + : []; + + const documentPromises = updateProjectDTO.documents + ? [ + ...project.documents.map((document) => { + const documentDTO = updateProjectDTO.documents.find( + (d) => d.id === document.id, + ); + if (!documentDTO) { + return Promise.all([ + this.documentRepository.delete({ + id: document.id, + projectId: project.id, + }), + this.uploadService.deleteObject( + document.url, + process.env.S3_PRIVATE_BUCKET_NAME, + ), + ]); + } + return Promise.resolve(); + }), + ...updateProjectDTO.documents.map((documentDTO) => { + const document = project.documents.find( + (d) => d.id === documentDTO.id, + ); + if (!document) { + return this.documentRepository.save({ + id: randomUUID(), + name: documentDTO.name, + url: documentDTO.url, + projectId: project.id, + }); + } + return Promise.resolve(); + }), + ] + : []; + + const screeningPromises = updateProjectDTO.screenings + ? [ + ...project.screenings.map((festival) => { + const festivalDTO = updateProjectDTO.screenings.find( + (f) => f.id === festival.id, + ); + return festivalDTO + ? this.festivalRepository.update( + { + id: festival.id, + projectId: project.id, + }, + { + year: festivalDTO.year, + festivalId: festivalDTO.festivalId, + }, + ) + : this.festivalRepository.delete({ + id: festival.id, + projectId: project.id, + }); + }), + ...updateProjectDTO.screenings.map((festivalDTO) => { + const festival = project.screenings.find( + (f) => f.id === festivalDTO.id, + ); + if (!festival) { + return this.festivalRepository.save({ + id: randomUUID(), + year: festivalDTO.year, + festivalId: festivalDTO.festivalId, + projectId: project.id, + }); + } + return Promise.resolve(); + }), + ] + : []; + + const subgenrePromises = updateProjectDTO.subgenres + ? [ + ...project.projectSubgenres.map((subgenre) => { + const subgenreDTO = updateProjectDTO.subgenres.find( + (sg) => sg === subgenre.subgenreId, + ); + if (!subgenreDTO) { + return this.projectGenreRepository.delete({ + subgenreId: subgenre.subgenreId, + projectId: project.id, + }); + } + return Promise.resolve(); + }), + ...updateProjectDTO.subgenres.map((subgenreDTO) => { + const subgenre = project.projectSubgenres.find( + (sg) => sg.subgenreId === subgenreDTO, + ); + if (!subgenre) { + return this.projectGenreRepository.save({ + id: randomUUID(), + subgenreId: subgenreDTO, + projectId: project.id, + }); + } + return Promise.resolve(); + }), + ] + : []; + + const posterPromise = + updateProjectDTO.poster && + updateProjectDTO.poster !== project.poster && + project.poster + ? this.uploadService.deleteObject( + project.poster, + process.env.S3_PUBLIC_BUCKET_NAME, + ) + : Promise.resolve(); + + const filmPromise = + updateProjectDTO.film && + (project.type === 'film' || project.type === 'music-video') + ? this.contentsService + .upsert(project.id, { + ...updateProjectDTO.film, + isRssEnabled: hasActiveRssAddon + ? updateProjectDTO.film.isRssEnabled + : false, + }) + .then(({ newMembers: contentMembers }) => { + return newMembers.push(...contentMembers); + }) + : Promise.resolve(); + + const episodePromises = + updateProjectDTO.episodes && project.type === 'episodic' + ? [ + ...project.contents.map((content) => { + const contentDTO = updateProjectDTO.episodes.find( + (c) => c.id === content.id, + ); + return contentDTO + ? this.contentsService + .upsert(project.id, contentDTO) + .then(({ newMembers: contentMembers }) => { + return newMembers.push(...contentMembers); + }) + : this.contentsService.remove(content.id); + }), + ...updateProjectDTO.episodes.map((contentDTO) => { + const content = project.contents.find( + (c) => c.id === contentDTO.id, + ); + if (!content) { + return this.contentsService.upsert(project.id, contentDTO); + } + return Promise.resolve(); + }), + ] + : []; + + const { existing, removed, added } = + updateProjectDTO.episodes && project.type === 'episodic' + ? this.seasonService.analizeSeasonDiff( + project.id, + updateProjectDTO.episodes, + project.contents.map((content) => ({ + projectId: content.projectId, + seasonNumber: content.season, + })), + ) + : { existing: [], removed: [], added: [] }; + + const removedSeasonsPromise = + updateProjectDTO.episodes && project.type === 'episodic' + ? this.seasonService.removeByProjectId( + project.id, + removed.map((s) => s.seasonNumber), + ) + : Promise.resolve(); + + const newSeasons = added.map((s) => ({ + seasonNumber: s.seasonNumber, + rentalPrice: updateProjectDTO.seasons.find( + (seasonToUpdate) => seasonToUpdate.seasonNumber === s.seasonNumber, + )?.rentalPrice, + })) as UpdateSeasonDto[]; + + const addedSeasonsPromise = this.seasonService.getOrCreateSeasonsUpsert( + project.id, + newSeasons, + ); + + const existingSeasonsPromise = this.seasonService.bulkUpdateSeasons( + existing.map((s) => { + const updatedSeason = updateProjectDTO.seasons.find( + (seasonToUpdate) => seasonToUpdate.seasonNumber === s.seasonNumber, + ); + return { + projectId: s.projectId, + seasonNumber: s.seasonNumber, + rentalPrice: updatedSeason?.rentalPrice, + title: updatedSeason?.title, + description: updatedSeason?.description, + isActive: updatedSeason?.isActive, + }; + }), + ); + const trailerQueueRemovalPromise = removeTrailerJobs + ? this.contentsService.removeManyTranscodingJobs([ + `project:${project.id}:trailer`, + ]) + : Promise.resolve(); + await Promise.all([ + ...permissionPromises, + ...awardPromises, + ...documentPromises, + ...screeningPromises, + ...subgenrePromises, + posterPromise, + trailerQueueRemovalPromise, + filmPromise, + ...episodePromises, + removedSeasonsPromise, + addedSeasonsPromise, + existingSeasonsPromise, + ]); + + const updatedProject = await this.findOne(id, fullRelations); + + if ( + updatedProject.status === 'under-review' && + previousProjectStatus !== 'under-review' + ) { + this.mailService.sendMail({ + to: this.configService.getOrThrow( + 'PROJECT_REVIEW_RECIPIENT_EMAIL', + ), + templateId: 'd-aaf2aa95950f430ba2e441aa883b34fb', + data: { + contentType: + updatedProject.type === 'episodic' + ? 'Episodic' + : updatedProject.type === 'film' + ? 'Film' + : 'Music Video', + contentTitle: updatedProject.title, + reviewDashboardUrl: + this.configService.getOrThrow('DASHBOARD_REVIEW_URL') + + updatedProject.id, + }, + }); + } + if (newMembers.length > 0) { + this.sendEmailToNewMembers(updatedProject, newMembers); + } + + if (isProjectRSSReady(project) || isProjectRSSReady(updatedProject)) + this.callPodping(project.id); + + if ( + shouldQueueProjectTrailer && + updatedProject.trailer?.file && + updatedProject.status === 'published' + ) { + await this.sendProjectTrailerToTranscodingQueue(updatedProject); + } + + return updatedProject; + } + + async remove(id: string) { + try { + const project = await this.projectsRepository.findOneOrFail({ + where: { id }, + relations: ['trailer'], + }); + if (project.poster) + await this.uploadService.deleteObject( + project.poster, + process.env.S3_PUBLIC_BUCKET_NAME, + ); + const trailerToRemove = project.trailer; + if (trailerToRemove?.file) { + await this.deleteTrailerAssets(trailerToRemove.file); + } + project.slug = project.id; + project.poster = ''; + // eslint-disable-next-line unicorn/no-null -- Remove the FK before deleting the trailer. + project.trailer = null; + await this.projectsRepository.save(project); + if (trailerToRemove) { + await this.trailerRepository.remove(trailerToRemove); + } + await this.contentsService.removeAll(project.id); + await this.contentsService.removeManyTranscodingJobs([ + `project:${project.id}:trailer`, + ]); + const result = await this.projectsRepository.softDelete({ id }); + this.callPodping(project.id); + return { success: result.affected > 0 }; + } catch (error) { + Logger.error(error); + return { success: false }; + } + } + + async stream(id: string) { + const project = await this.findOne(id); + return project; + } + + async sendProjectTrailerToTranscodingQueue(project: Project) { + if (!project.trailer?.file) return; + await this.contentsService.removeManyTranscodingJobs([ + `project:${project.id}:trailer`, + ]); + Logger.log(`Sending project trailer ${project.id} to transcoding queue`); + + await this.trailerRepository.update(project.trailer.id, { + status: 'processing', + metadata: undefined, + }); + + const callbackProtocol = + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ? 'http' + : 'https'; + + await this.transcodeQueue.add('transcode', { + inputBucket: process.env.S3_PRIVATE_BUCKET_NAME, + outputBucket: process.env.S3_PUBLIC_BUCKET_NAME, + inputKey: project.trailer.file, + outputKey: getTrailerTranscodeOutputKey(project.trailer.file), + correlationId: `project:${project.id}:trailer`, + callbackUrl: `${callbackProtocol}://${process.env.DOMAIN}/projects/${project.id}/trailer/transcoding`, + }); + } + + async trailerTranscodingCompleted( + id: string, + status: ContentStatus, + metadata: any, + ) { + Logger.log( + `Project trailer transcoding completed for project ${id}, status: ${status}`, + ); + const project = await this.projectsRepository.findOne({ + where: { id }, + relations: ['trailer'], + }); + if (!project?.trailer) return false; + await this.trailerRepository.update(project.trailer.id, { + status, + metadata, + }); + return true; + } + + private async deleteTrailerAssets(trailerFile: string) { + await Promise.all([ + this.uploadService.deleteObject( + trailerFile, + process.env.S3_PRIVATE_BUCKET_NAME, + ), + this.uploadService.deleteObject( + trailerFile, + process.env.S3_PUBLIC_BUCKET_NAME, + ), + this.uploadService.deleteObject( + getTrailerTranscodedFileRoute(trailerFile), + process.env.S3_PRIVATE_BUCKET_NAME, + ), + this.uploadService.deleteObject( + getTrailerTranscodedFileRoute(trailerFile), + process.env.S3_PUBLIC_BUCKET_NAME, + ), + ]); + } + + async getProjectsRevenue( + projectIds: string[], + filmmakerId: string, + dateRange: FilterDateRange, + ) { + let projects: Project[] = []; + projects = await (projectIds.length === 0 + ? this.findAll({ + filmmakerId: filmmakerId, + limit: undefined, + offset: 0, + show: 'shareholder', + relations: true, + }) + : this.findMany(projectIds, ['permissions'])); + + const ids = projects.map((project) => project.id); + + const isOwner = projects.every((project) => + project.permissions.some( + (permission) => + permission.role === 'owner' && permission.filmmakerId === filmmakerId, + ), + ); + + const { startDate } = getFilterDateRange(dateRange); + const projectRevenueAnalytics = + await this.contentsService.getProjectsRevenueAnalytics( + ids, + filmmakerId, + startDate, + ); + + Logger.log({ projectRevenueAnalytics }); + + return { + isOwner, + total: isOwner ? projectRevenueAnalytics.total : undefined, + user: projectRevenueAnalytics.user, + }; + } + + async resendInvite(projectId: string, email: string) { + const project = await this.projectsRepository.findOneOrFail({ + where: { id: projectId }, + relations: [ + 'permissions', + 'permissions.filmmaker', + 'contents', + 'contents.cast', + 'contents.crew', + 'contents.invites', + ], + }); + + const owner = project.permissions.find((p) => p.role === 'owner'); + + const exists = + project.contents.some( + (content) => + content.cast.some((c) => c.email === email) || + content.crew.some((c) => c.email === email) || + content.invites.some((invite) => invite.email === email), + ) || project.permissions.some((p) => p.email === email); + + if (!exists) { + throw new NotFoundException('Email not found in invites'); + } + this.mailService.sendMail({ + to: email, + templateId: 'd-4cfa6bdc5a37440a993623162243a0ed', + data: { + invitedBy: owner.filmmaker.professionalName, + filmName: project.title, + }, + }); + } + + async checkIfSlugExists(projectId: string, slug: string) { + const exists = await this.projectsRepository.exists({ + where: { slug, id: Not(projectId) }, + }); + + return { exists }; + } + + async sendEmailToNewMembers( + project: Project, + newMembers: { id?: string; email?: string }[], + ) { + const sent = new Set(); + const owner = project.permissions.find((p) => p.role === 'owner').filmmaker; + sent.add(owner.user.email); + + const allCast = project.contents.flatMap((c) => c.cast); + const allCrew = project.contents.flatMap((c) => c.crew); + const allShareholders = project.contents.flatMap((c) => c.shareholders); + + for (const member of newMembers) { + if (member.id) { + const foundMember = + allCast.find((c) => c.filmmakerId === member.id) || + allCrew.find((c) => c.filmmakerId === member.id) || + allShareholders.find((s) => s.filmmakerId === member.id) || + project.permissions.find((p) => p.filmmakerId === member.id); + + if (foundMember && !sent.has(member.email)) { + this.mailService.sendMail({ + to: foundMember.filmmaker.user.email, + templateId: 'd-2adecb2db5e34faab869427ee36d4b96', + data: { + invitedBy: owner.professionalName, + filmName: project.title, + }, + }); + sent.add(member.email); + } + } else if (member.email && !sent.has(member.email)) { + this.mailService.sendMail({ + to: member.email, + templateId: 'd-4cfa6bdc5a37440a993623162243a0ed', + data: { + invitedBy: owner.professionalName, + }, + }); + sent.add(member.email); + } + } + } + + async updateOnRegister(email: string, filmmakerId: string) { + await this.permissionRepository.update( + { email }, + { + filmmakerId, + email: undefined, + }, + ); + } + + async callPodping(id: string) { + if (process.env.ENVIRONMENT !== 'production') return; + const url = `https://${process.env.DOMAIN}/rss/${id}`; + axios + .get(process.env.PODPING_URL, { + params: { + url, + medium: 'film', + }, + headers: { + 'User-Agent': process.env.PODPING_USER_AGENT, + Authorization: process.env.PODPING_KEY, + }, + }) + .then((response) => { + Logger.log('Notified for id ' + id, 'Podping'); + return response; + }) + .catch((error) => { + Logger.error('Podping error', error); + }); + } + + async adminValidation( + id: string, + status: Status, + rejectedReason?: string, + permissions?: AddPermissionDTO[], + ) { + let project = await this.projectsRepository.findOneOrFail({ + where: { id }, + relations: [ + 'permissions', + 'contents', + 'permissions.filmmaker', + 'permissions.filmmaker.user', + 'projectSubgenres', + 'projectSubgenres.subgenre', + ], + }); + const ownerEmail = project.permissions.find((p) => p.role === 'owner') + .filmmaker.user.email; + if (status === 'rejected') { + project.status = status; + project.rejectedReason = rejectedReason; + await this.mailService.sendMail({ + to: ownerEmail, + templateId: 'd-276708447ec14a4591c584fd47e37e95', + data: { + filmName: project.title, + rejectedReason, + }, + }); + } else if (status === 'published') { + project.status = status; + project.rejectedReason = undefined; + for (const content of project.contents) { + this.contentsService.sendToTranscodingQueue(content, project); + if (content.trailer?.file) { + this.contentsService.sendTrailerToTranscodingQueue(content, project); + } + } + if (project.trailer?.file) { + this.sendProjectTrailerToTranscodingQueue(project); + } + } + await this.projectsRepository.save(project); + + if (permissions) { + project = await this.update( + id, + { + permissions, + status: project.status, + subgenres: project.projectSubgenres.map((sg) => sg.subgenreId), + }, + true, + ); + } + return project; + } + + async removeFilmmaker(filmmakerId: string) { + const projects = await this.projectsRepository.find({ + relations: ['permissions'], + where: { + permissions: { + filmmakerId, + }, + }, + }); + + for (const project of projects) { + const permission = project.permissions.find( + (p) => p.filmmakerId === filmmakerId, + ); + if (permission) { + await this.permissionRepository.delete({ + projectId: project.id, + filmmakerId, + }); + } + } + } +} diff --git a/backend/src/rents/config/constants.ts b/backend/src/rents/config/constants.ts new file mode 100644 index 0000000..766f648 --- /dev/null +++ b/backend/src/rents/config/constants.ts @@ -0,0 +1 @@ +export const REVENUE_PERCENTAGE_TO_PAY = 0.7; diff --git a/backend/src/rents/dto/request/film-rent.dto.ts b/backend/src/rents/dto/request/film-rent.dto.ts new file mode 100644 index 0000000..2042e6e --- /dev/null +++ b/backend/src/rents/dto/request/film-rent.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class FilmRentDTO { + @IsString() + id: string; +} diff --git a/backend/src/rents/dto/request/list-films.dto.ts b/backend/src/rents/dto/request/list-films.dto.ts new file mode 100644 index 0000000..59ed4df --- /dev/null +++ b/backend/src/rents/dto/request/list-films.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; + +export class ListRentsDTO { + @ApiProperty() + @IsOptional() + limit = 30; + + @ApiProperty() + @IsOptional() + offset = 0; + + @ApiProperty() + @IsOptional() + sort?: string; + + @ApiProperty() + @IsOptional() + @IsEnum(['active', 'expired']) + status?: 'expired' | 'active'; + + @ApiProperty() + @IsOptional() + projectId?: string; +} diff --git a/backend/src/rents/dto/request/season-rent.dto.ts b/backend/src/rents/dto/request/season-rent.dto.ts new file mode 100644 index 0000000..4875547 --- /dev/null +++ b/backend/src/rents/dto/request/season-rent.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class SeasonRentRequestDTO { + @IsString() + id: string; +} diff --git a/backend/src/rents/dto/response/rent.dto.ts b/backend/src/rents/dto/response/rent.dto.ts new file mode 100644 index 0000000..4475e24 --- /dev/null +++ b/backend/src/rents/dto/response/rent.dto.ts @@ -0,0 +1,28 @@ +import { ProjectDTO } from 'src/projects/dto/response/project.dto'; +import { Rent } from 'src/rents/entities/rent.entity'; + +export class RentDTO { + id: string; + status: 'active' | 'expired'; + usdAmount: number; + lightningAmount: number; + project: ProjectDTO; + createdAt: Date; + expiresAt: Date; + title: string; + + constructor(rent: Rent) { + this.id = rent.id; + const expiresAt = new Date(rent.createdAt); + expiresAt.setDate(expiresAt.getDate() + 2); + this.status = + rent.status == 'paid' && new Date() < expiresAt ? 'active' : 'expired'; + this.usdAmount = rent.usdAmount; + if (rent.content) { + this.project = new ProjectDTO(rent.content.project); + this.title = rent.content?.title || rent.content?.project?.title || ''; + } + this.createdAt = rent.createdAt; + this.expiresAt = expiresAt; + } +} diff --git a/backend/src/rents/entities/rent.entity.ts b/backend/src/rents/entities/rent.entity.ts new file mode 100644 index 0000000..1e38a08 --- /dev/null +++ b/backend/src/rents/entities/rent.entity.ts @@ -0,0 +1,47 @@ +import { Content } from 'src/contents/entities/content.entity'; +import { ColumnNumericTransformer } from 'src/database/transformers/column-numeric-transformer'; +import { User } from 'src/users/entities/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; + +@Entity('rents') +export class Rent { + @PrimaryColumn() + id: string; + + @PrimaryColumn() + contentId: string; + + @PrimaryColumn() + userId: string; + + @Column('decimal', { + precision: 5, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + usdAmount: number; + + @Column() + status: 'pending' | 'paid' | 'cancelled'; + + @Column({ nullable: true }) + providerId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @ManyToOne(() => Content, (content) => content.rents) + @JoinColumn({ name: 'content_id' }) + content: Content; + + @ManyToOne(() => User, (user) => user.rents) + @JoinColumn({ name: 'user_id' }) + user: User; +} diff --git a/backend/src/rents/guards/for-rental.guard.ts b/backend/src/rents/guards/for-rental.guard.ts new file mode 100644 index 0000000..da675b8 --- /dev/null +++ b/backend/src/rents/guards/for-rental.guard.ts @@ -0,0 +1,30 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, +} from '@nestjs/common'; +import { ContentsService } from 'src/contents/contents.service'; +import { fullRelations } from 'src/contents/entities/content.entity'; + +@Injectable() +export class ForRentalGuard implements CanActivate { + constructor( + @Inject(ContentsService) + private contentsService: ContentsService, + ) {} + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const contentId = request.body.id || request.params.id; + + if (contentId) { + const content = await this.contentsService.findOne( + contentId, + fullRelations, + ); + return content?.rentalPrice > 0; + } else { + return false; + } + } +} diff --git a/backend/src/rents/guards/for-season-rental.guard.ts b/backend/src/rents/guards/for-season-rental.guard.ts new file mode 100644 index 0000000..a0ee965 --- /dev/null +++ b/backend/src/rents/guards/for-season-rental.guard.ts @@ -0,0 +1,26 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, +} from '@nestjs/common'; +import { SeasonService } from 'src/season/season.service'; + +@Injectable() +export class ForSeasonRentalGuard implements CanActivate { + constructor( + @Inject(SeasonService) + private seasonService: SeasonService, + ) {} + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const seasonId = request.body.id || request.params.id; + + if (seasonId) { + const season = await this.seasonService.findOne(seasonId); + return season?.rentalPrice > 0; + } else { + return false; + } + } +} diff --git a/backend/src/rents/rents.controller.ts b/backend/src/rents/rents.controller.ts new file mode 100644 index 0000000..4a66375 --- /dev/null +++ b/backend/src/rents/rents.controller.ts @@ -0,0 +1,93 @@ +import { + Controller, + Post, + UseGuards, + Param, + Patch, + Body, + Get, + Query, + NotFoundException, +} from '@nestjs/common'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { ForRentalGuard } from './guards/for-rental.guard'; +import { RentsService } from './rents.service'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { FilmRentDTO } from './dto/request/film-rent.dto'; +import { RentDTO } from './dto/response/rent.dto'; +import { ListRentsDTO } from './dto/request/list-films.dto'; +import { CouponCodeDto } from 'src/discounts/dto/request/coupon-code.dto'; + +@Controller('rents') +export class RentsController { + constructor(private readonly rentsService: RentsService) {} + + @Get() + @UseGuards(HybridAuthGuard) + async find(@User() user: RequestUser['user'], @Query() query: ListRentsDTO) { + const rents = await this.rentsService.findByUserId(user.id, query); + return rents.map((rent) => new RentDTO(rent)); + } + + @Get('/count') + @UseGuards(HybridAuthGuard) + async findCount( + @User() user: RequestUser['user'], + @Query() query: ListRentsDTO, + ) { + const count = await this.rentsService.getCountByUserId(user.id, query); + return { count }; + } + + @Post('/lightning') + @UseGuards(HybridAuthGuard, ForRentalGuard) + async lightningRent( + @Body() { id }: FilmRentDTO, + @User() user: RequestUser['user'], + @Body() { couponCode }: CouponCodeDto, + ) { + const quote = await this.rentsService.createLightningInvoice( + id, + user.email, + user.id, + couponCode, + ); + return quote; + } + + @Patch('/lightning/:id/quote') + @UseGuards(HybridAuthGuard) + async createQuote(@Param('id') invoiceId: string) { + const quote = await this.rentsService.createLightningQuote(invoiceId); + return quote; + } + + @Get('/content/:id') + @UseGuards(HybridAuthGuard) + async findFilm( + @Param('id') contentId: string, + @User() user: RequestUser['user'], + ) { + try { + const rent = await this.rentsService.getByContentId(user.id, contentId); + return new RentDTO(rent); + } catch { + throw new NotFoundException('Rent not found'); + } + } + + @Get('/content/:id/exists') + @UseGuards(HybridAuthGuard) + async filmRentExists( + @Param('id') contentId: string, + @User() { id }: RequestUser['user'], + ) { + try { + const exists = await this.rentsService.rentByUserExists(id, contentId); + return { exists }; + } catch { + return { exists: false }; + } + } +} diff --git a/backend/src/rents/rents.module.ts b/backend/src/rents/rents.module.ts new file mode 100644 index 0000000..00f4e35 --- /dev/null +++ b/backend/src/rents/rents.module.ts @@ -0,0 +1,35 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Rent } from './entities/rent.entity'; +import { RentsController } from './rents.controller'; +import { RentsService } from './rents.service'; +import { PaymentModule } from 'src/payment/payment.module'; +import { EventsModule } from 'src/events/events.module'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { ContentsModule } from 'src/contents/contents.module'; +import { SeasonService } from 'src/season/season.service'; +import { Season } from 'src/season/entities/season.entity'; +import { SeasonRent } from 'src/season/entities/season-rents.entity'; +import { Discount } from 'src/discounts/entities/discount.entity'; +import { DiscountRedemption } from 'src/discount-redemption/entities/discount-redemption.entity'; +import { DiscountRedemptionService } from 'src/discount-redemption/discount-redemption.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Rent, + Shareholder, + Season, + SeasonRent, + Discount, + DiscountRedemption, + ]), + PaymentModule, + ContentsModule, + EventsModule, + ], + controllers: [RentsController], + providers: [RentsService, SeasonService, DiscountRedemptionService], + exports: [RentsService], +}) +export class RentsModule {} diff --git a/backend/src/rents/rents.service.ts b/backend/src/rents/rents.service.ts new file mode 100644 index 0000000..1f321b4 --- /dev/null +++ b/backend/src/rents/rents.service.ts @@ -0,0 +1,278 @@ +import { + BadRequestException, + Inject, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IsNull, MoreThanOrEqual, Repository } from 'typeorm'; +import { Rent } from './entities/rent.entity'; +import { BTCPayService } from 'src/payment/providers/services/btcpay.service'; +import { randomUUID } from 'node:crypto'; +import { UsersService } from 'src/users/users.service'; +import { ListRentsDTO } from './dto/request/list-films.dto'; +import { ContentsService } from 'src/contents/contents.service'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { REVENUE_PERCENTAGE_TO_PAY } from './config/constants'; +import { Content } from 'src/contents/entities/content.entity'; +import { RentsGateway } from 'src/events/rents.gateway'; +import Invoice from 'src/payment/providers/dto/strike/invoice'; +import { Discount } from 'src/discounts/entities/discount.entity'; +import { DiscountRedemptionService } from 'src/discount-redemption/discount-redemption.service'; +import { calculateDiscountedPrice } from 'src/discounts/utils/calculate-discount-price'; +import { isValidDiscountedPrice } from 'src/discounts/utils/is-valid-discount'; + +@Injectable() +export class RentsService { + constructor( + @InjectRepository(Rent) + private rentRepository: Repository, + @InjectRepository(Discount) + private discountRepository: Repository, + @InjectRepository(Shareholder) + private shareholdersRepository: Repository, + @Inject(BTCPayService) + private readonly btcpayService: BTCPayService, + @Inject(ContentsService) + private readonly contentsService: ContentsService, + @Inject(UsersService) + private readonly usersService: UsersService, + @Inject(DiscountRedemptionService) + private readonly discountRedemptionService: DiscountRedemptionService, + @Inject(RentsGateway) + private readonly eventsGateway: RentsGateway, + ) {} + + async findByUserId(userId: string, query: ListRentsDTO) { + const rentsQuery = await this.getRentsQuery(userId, query); + rentsQuery.limit(query.limit); + rentsQuery.offset(query.offset); + return rentsQuery.getMany(); + } + + async getByContentId(userId: string, contentId: string) { + return this.rentRepository.findOneOrFail({ + where: { contentId, userId, status: 'paid' }, + order: { createdAt: 'DESC' }, + }); + } + + async rentByUserExists(userId: string, contentId: string) { + return this.rentRepository.exists({ + where: { + contentId, + userId, + createdAt: MoreThanOrEqual(this.getExpiringDate()), + status: 'paid', + }, + }); + } + + async getCountByUserId(userId: string, query: ListRentsDTO) { + const rentsQuery = await this.getRentsQuery(userId, query); + return rentsQuery.getCount(); + } + + async getRentsQuery(userId: string, query: ListRentsDTO) { + const rentsQuery = this.rentRepository.createQueryBuilder('rent'); + rentsQuery.withDeleted(); + rentsQuery.leftJoinAndSelect('rent.content', 'content'); + rentsQuery.leftJoinAndSelect('content.project', 'project'); + rentsQuery.where('rent.userId = :userId', { userId }); + rentsQuery.andWhere('rent.status = :status', { status: 'paid' }); + rentsQuery.andWhere('project.deleted_at IS NULL'); + if (query.status === 'active') { + rentsQuery.andWhere('rent.createdAt > :date', { + date: this.getExpiringDate(), + }); + } else if (query.status === 'expired') { + rentsQuery.andWhere('rent.createdAt < :date', { + date: this.getExpiringDate(), + }); + } + rentsQuery.orderBy('rent.createdAt', 'DESC'); + return rentsQuery; + } + + async rentByProviderIdExists(providerId: string) { + return this.rentRepository.exists({ + where: { providerId }, + }); + } + + async createLightningInvoice( + contentId: string, + userEmail: string, + userId: string, + couponCode?: string, + ) { + const content = await this.contentsService.findOne(contentId); + + const { + finalPrice, + discount, + rent: freeRental, + } = await this.processDiscount(userId, content, couponCode, userEmail); + + if (discount?.type === 'free') { + return { + ...freeRental, + }; + } + + const invoice = await this.btcpayService.issueInvoice( + finalPrice, + 'Invoice for order', + randomUUID(), + ); + const rent = await this.rentRepository.save({ + id: randomUUID(), + contentId, + userId, + usdAmount: finalPrice, + status: 'pending', + providerId: invoice.invoiceId, + }); + const quote = await this.createLightningQuote(invoice.invoiceId); + return { + ...rent, + sourceAmount: quote.sourceAmount, + conversionRate: quote.conversionRate, + expiration: quote.expiration, + lnInvoice: quote.lnInvoice, + }; + } + + async createLightningQuote(invoiceId: string) { + return this.btcpayService.issueQuote(invoiceId); + } + + async lightningPaid(invoiceId: string, invoice?: Invoice) { + if (!invoice) { + invoice = await this.btcpayService.getInvoice(invoiceId); + } + const rent = await this.rentRepository.findOne({ + where: { providerId: invoiceId }, + }); + if (!rent) { + throw new BadRequestException('Rent not found'); + } + if (invoice.state === 'PAID') { + rent.status = 'paid'; + await this.rentRepository.save(rent); + await this.payShareholders(rent.contentId, rent.usdAmount); + this.eventsGateway.server.emit(`${rent.id}`, { + invoiceId, + rentId: rent.id, + }); + } else { + throw new BadRequestException('Invoice not paid'); + } + } + + async payShareholders(contentId: string, amount: number) { + const total = amount * REVENUE_PERCENTAGE_TO_PAY; + await this.shareholdersRepository.update( + { + contentId, + deletedAt: IsNull(), + }, + { + rentPendingRevenue: () => + `rent_pending_revenue + (cast(share as decimal) / 100.00 * ${total})`, + }, + ); + } + + private getExpiringDate() { + return new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); + } + + async processDiscount( + userId: string, + content: Content, + couponCode?: string, + email?: string, + ) { + let finalPrice = content.rentalPrice; + let rent: Rent | undefined; + if (couponCode) { + const discount = await this.discountRepository.findOne({ + where: { + couponCode, + contentId: content.id, + deletedAt: IsNull(), + }, + }); + + const discountedPrice = calculateDiscountedPrice( + discount.type, + discount.value, + content.rentalPrice, + ); + + if (!isValidDiscountedPrice(discountedPrice, discount.type)) { + throw new UnprocessableEntityException('Invalid discount'); + } + finalPrice = discountedPrice; + + const canRedeem = await this.discountRedemptionService.canRedeem( + discount.createdById, + discount.type, + { contentId: content.id }, + ); + + if (!canRedeem) { + let errorMessage = + 'Maximum free redemptions reached for this filmmaker'; + if (discount?.email !== email) { + errorMessage = 'Invalid discount'; + } + this.eventsGateway.server.emit('error', { + errorMessage, + }); + throw new UnprocessableEntityException(errorMessage); + } + + if (discount.type === 'free') { + rent = await this.rentRepository.save({ + id: randomUUID(), + contentId: content.id, + userId, + usdAmount: finalPrice, + status: 'paid', + providerId: undefined, + }); + + this.eventsGateway.server.emit('free', { + invoiceId: rent.id, + rentId: rent.id, + }); + } + + await this.discountRedemptionService.createRedemption({ + userId, + contentId: content.id, + usdAmount: finalPrice, + couponCode, + discount: { + id: discount.id, + type: discount.type, + createdById: discount.createdById, + }, + }); + + return { + finalPrice, + discount, + rent, + }; + } else { + return { + finalPrice, + discount: undefined, + rent: undefined, + }; + } + } +} diff --git a/backend/src/rss/rss.controller.ts b/backend/src/rss/rss.controller.ts new file mode 100644 index 0000000..4cac36c --- /dev/null +++ b/backend/src/rss/rss.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Header, Param } from '@nestjs/common'; +import { RssService } from './rss.service'; + +@Controller('rss') +export class RssController { + constructor(private readonly rssService: RssService) {} + + @Get(':id') + @Header('Content-Type', 'application/rss+xml') + getProjectRssFeed(@Param('id') id: string) { + return this.rssService.generateProjectRssFeed(id); + } +} diff --git a/backend/src/rss/rss.module.ts b/backend/src/rss/rss.module.ts new file mode 100644 index 0000000..d48b6d9 --- /dev/null +++ b/backend/src/rss/rss.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { RssController } from './rss.controller'; +import { RssService } from './rss.service'; +import { ProjectsModule } from 'src/projects/projects.module'; +import { ContentsModule } from 'src/contents/contents.module'; + +@Module({ + providers: [RssService], + imports: [ProjectsModule, ContentsModule], + controllers: [RssController], +}) +export class RssModule {} diff --git a/backend/src/rss/rss.service.ts b/backend/src/rss/rss.service.ts new file mode 100644 index 0000000..4fd18b0 --- /dev/null +++ b/backend/src/rss/rss.service.ts @@ -0,0 +1,303 @@ +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import * as RSS from 'rss'; +import { Project, fullRelations } from 'src/projects/entities/project.entity'; +import { Content } from 'src/contents/entities/content.entity'; + +import { ProjectsService } from 'src/projects/projects.service'; +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { + getPublicS3Url, + getTrailerTranscodedFileRoute, + getTranscodedFileRoute, + isProjectRSSReady, +} from 'src/common/helper'; +import axios from 'axios'; + +@Injectable() +export class RssService { + constructor( + @Inject(ProjectsService) + private readonly projectsService: ProjectsService, + ) {} + + async generateProjectRssFeed(id: string) { + const project = await this.projectsService.findOne(id, fullRelations); + + if (!isProjectRSSReady(project)) { + throw new NotFoundException('Project not found'); + } + + const trailerUrl = + project.trailer?.file && project.trailer.status === 'completed' + ? getPublicS3Url(getTrailerTranscodedFileRoute(project.trailer.file)) + : undefined; + + const feed = new RSS({ + title: project.title, + description: project.synopsis, + generator: 'IndeeHub RSS', + webMaster: 'hello@indeehub.studio', + feed_url: `https://${process.env.DOMAIN}/rss/${project.id}`, + site_url: `https://${process.env.DOMAIN}/rss/${project.id}`, + pubDate: project.updatedAt.toISOString(), + image_url: this.getPoster(project), + ...this.generateDefaultElements(), + categories: project.projectSubgenres.map((pg) => pg.subgenre.name), + custom_elements: this.generateProjectElements(project, trailerUrl), + }); + + for (const content of project.contents) { + feed.item(await this.generateEpisodicItem(content, project)); + } + return feed.xml(); + } + + async generateEpisodicItem( + content: Content, + project: Project, + ): Promise { + return { + title: content.title, + description: content.synopsis, + url: this.getSiteUrl(project.slug), + guid: content.id, + date: project.updatedAt.toISOString(), + custom_elements: await this.generateContentElements(content, project), + enclosure: { + url: this.getTranscodedUrl(content), + type: 'application/x-mpegURL', + }, + }; + } + + generateProjectElements(project: Project, trailerUrl?: string) { + const owner = project.permissions.find((p) => p.role === 'owner'); + const elements: object[] = [ + { + 'podcast:guid': project.id, + }, + { 'podcast:medium': 'film' }, + { 'podcast:podping': { _attr: { usesPodping: 'true' } } }, + { 'podcast:publisher': 'IndeeHub' }, + { author: owner.filmmaker.professionalName }, + { + 'podcast:locked': { + _attr: { owner: owner.filmmaker.user.email }, + _cdata: 'yes', + }, + }, + { + 'podcast:block': { + _cdata: 'yes', + }, + }, + { + 'itunes:author': owner.filmmaker.professionalName, + }, + { + 'itunes:owner': [ + { 'itunes:name': owner.filmmaker.professionalName }, + { 'itunes:email': owner.filmmaker.user.email }, + ], + }, + ]; + + if (project.trailer?.file && trailerUrl) { + elements.push({ + 'podcast:trailer': { + _attr: { + pubdate: project.updatedAt.toISOString(), + url: trailerUrl, + type: 'video/mp4', + }, + _cdata: `Trailer for ${project.title}`, + }, + }); + } + + if (project.projectSubgenres) { + const genres: any[] = []; + for (const projectGenre of project.projectSubgenres) { + genres.push({ + 'itunes:category': { + _attr: { text: projectGenre.subgenre.name }, + }, + }); + } + elements.push({ + 'itunes:category': [{ _attr: { text: 'TV & Film' } }, ...genres], + }); + } + + // for (const content of project.contents) { + // elements.push(...this.generateCastCrewElements(content)); + // } + return elements; + } + + async generateContentElements(content: Content, project: Project) { + const elements: object[] = [ + { + 'podcast:images': { + _attr: { + srcset: `${getPublicS3Url(project.poster)} 500w`, + }, + }, + }, + // ...this.generateCastCrewElements(content), + await this.generateValueElements(content), + ]; + + if (project.type === 'episodic') { + elements.push(...this.generateEpisodicElements(content)); + } + return elements; + } + + generateCastCrewElements(content: Content) { + // can also go in project + const cast = content.cast.map((c) => ({ + 'podcast:person': { + _attr: { + group: 'cast', + role: c.character, + href: this.getHref(c.filmmaker), + img: this.getImg(c.filmmaker), + }, + _cdata: c.filmmaker?.professionalName ?? c.placeholderName, + }, + })); + + const crew = content.crew.map((c) => ({ + 'podcast:person': { + _attr: { + group: 'crew', + role: c.occupation, + href: this.getHref(c.filmmaker), + img: this.getImg(c.filmmaker), + }, + _cdata: c.filmmaker?.professionalName ?? c.placeholderName, + }, + })); + + return [...cast, ...crew]; + } + + generateContentTranscript(content: Content) { + return content.captions.map((caption) => { + return { + 'podcast:transcript': { + __attr: { + url: caption.url, + type: 'text/vtt', + language: caption.language, + }, + }, + }; + }); + } + + async generateValueElements(content: Content) { + const valueRecipientsPromises = content.rssShareholders.map(async (s) => { + let address = s.nodePublicKey; + let customKey = s.key; + let customValue = s.value; + if (s.lightningAddress) { + try { + const { data } = await axios.get( + 'https://api.getalby.com/lnurl/lightning-address-details', + { + params: { + ln: s.lightningAddress, + }, + }, + ); + if (data.keysend) { + address = data.keysend.pubkey; + if (data.keysend.customData) { + customKey = data.keysend.customData[0]?.customKey; + customValue = data.keysend.customData[0]?.customValue; + } + } + } catch (error) { + console.error(error); + } + } + return { + 'podcast:valueRecipient': { + _attr: { + type: 'node', + address, + name: s.name ?? s.lightningAddress, + customKey, + customValue, + split: s.share.toString(), + // fee: s.fee ? 'true' : undefined, // Only include the fee attribute if it's true + }, + }, + }; + }); + const valueRecipients = await Promise.all(valueRecipientsPromises); + const filteredRecipients = valueRecipients.filter( + (recipient) => recipient['podcast:valueRecipient']._attr.address, + ); + return { + 'podcast:value': [ + { + _attr: { + type: 'lightning', + method: 'keysend', + suggested: '0.00000015000', + }, + }, + ...filteredRecipients, + ], + }; + } + + generateEpisodicElements(content: Content) { + return [ + { 'podcast:season': content.season + 1 }, + { + 'podcast:episode': { + _attr: { display: content.title }, + _cdata: content.order, + }, + }, + ]; + } + + generateDefaultElements() { + return { + language: 'en-US', + custom_namespaces: { + podcast: + 'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md', + itunes: 'https://www.itunes.com/dtds/podcast-1.0.dtd', + }, + }; + } + + private getSiteUrl(slug: string) { + return `${process.env.FRONTEND_URL}/film/${slug}`; + } + + private getHref(filmmaker?: Filmmaker) { + if (!filmmaker) return process.env.FRONTEND_URL; + return `${process.env.FRONTEND_URL}/filmmakers/${filmmaker.id}`; + } + + private getImg(filmmaker?: Filmmaker) { + if (!filmmaker?.user?.profilePictureUrl) return ''; + return getPublicS3Url(filmmaker.user.profilePictureUrl); + } + + private getPoster(project: Project) { + if (!project.poster) return ''; + return getPublicS3Url(project.poster); + } + + private getTranscodedUrl(content: Content) { + return getPublicS3Url(getTranscodedFileRoute(content.file)); + } +} diff --git a/backend/src/scripts/seed-content.ts b/backend/src/scripts/seed-content.ts new file mode 100644 index 0000000..56f6477 --- /dev/null +++ b/backend/src/scripts/seed-content.ts @@ -0,0 +1,305 @@ +/** + * Database Seed Script + * + * Populates the PostgreSQL database with: + * 1. Genres (Documentary, Drama, etc.) + * 2. Test users with Nostr pubkeys and active subscriptions + * 3. IndeeHub films (native delivery mode) + * 4. TopDoc films (native delivery mode, YouTube streaming URLs) + * 5. Projects and contents for both film sets + * + * Run: node dist/scripts/seed-content.js + * Requires: DATABASE_HOST, DATABASE_PORT, DATABASE_USER, etc. in env + */ + +import { Client } from 'pg'; +import { randomUUID } from 'node:crypto'; + +const client = new Client({ + host: process.env.DATABASE_HOST || 'localhost', + port: Number(process.env.DATABASE_PORT || '5432'), + user: process.env.DATABASE_USER || 'indeedhub', + password: process.env.DATABASE_PASSWORD || 'indeedhub_dev_2026', + database: process.env.DATABASE_NAME || 'indeedhub', +}); + +// ── Test Users ──────────────────────────────────────────────── +// Using the same dev personas from the frontend seed +const testUsers = [ + { + id: randomUUID(), + email: 'alice@indeedhub.local', + legalName: 'Alice Developer', + nostrPubkey: + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', + }, + { + id: randomUUID(), + email: 'bob@indeedhub.local', + legalName: 'Bob Filmmaker', + nostrPubkey: + 'b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3', + }, + { + id: randomUUID(), + email: 'charlie@indeedhub.local', + legalName: 'Charlie Audience', + nostrPubkey: + 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + }, +]; + +// ── Genres ──────────────────────────────────────────────────── +const genres = [ + { id: randomUUID(), name: 'Documentary', type: 'film' }, + { id: randomUUID(), name: 'Drama', type: 'film' }, + { id: randomUUID(), name: 'Action', type: 'film' }, + { id: randomUUID(), name: 'Horror', type: 'film' }, + { id: randomUUID(), name: 'Comedy', type: 'film' }, + { id: randomUUID(), name: 'Thriller', type: 'film' }, + { id: randomUUID(), name: 'Science Fiction', type: 'film' }, + { id: randomUUID(), name: 'Animation', type: 'film' }, +]; + +// ── IndeeHub Films ──────────────────────────────────────────── +const indeeHubFilms = [ + { + id: 'god-bless-bitcoin', + title: 'God Bless Bitcoin', + synopsis: + 'A groundbreaking documentary exploring the intersection of faith, finance, and the future of money through the lens of Bitcoin.', + poster: '/images/films/posters/god-bless-bitcoin.webp', + genre: 'Documentary', + categories: ['Documentary', 'Bitcoin', 'Religion'], + deliveryMode: 'native', + }, + { + id: 'thethingswecarry', + title: 'The Things We Carry', + synopsis: + 'A compelling narrative exploring the emotional weight of our past.', + poster: '/images/films/posters/thethingswecarry.webp', + genre: 'Drama', + categories: ['Drama'], + deliveryMode: 'native', + }, + { + id: 'duel', + title: 'Duel', + synopsis: 'An intense confrontation that tests the limits of human resolve.', + poster: '/images/films/posters/duel.png', + genre: 'Action', + categories: ['Drama', 'Action'], + deliveryMode: 'native', + }, + { + id: '2b0d7349-c010-47a0-b584-49e1bf86ab2f', + title: 'Hard Money', + synopsis: + 'Understanding sound money principles and monetary sovereignty.', + poster: + '/images/films/posters/2b0d7349-c010-47a0-b584-49e1bf86ab2f.png', + genre: 'Documentary', + categories: ['Documentary', 'Finance', 'Bitcoin'], + deliveryMode: 'native', + }, +]; + +// ── TopDoc Films ────────────────────────────────────────────── +const topDocFilms = [ + { + id: 'tdf-god-bless-bitcoin', + title: 'God Bless Bitcoin', + synopsis: + 'Exploring the intersection of faith and Bitcoin.', + poster: '/images/films/posters/topdoc/god-bless-bitcoin.jpg', + streamingUrl: 'https://www.youtube.com/embed/3XEuqixD2Zg', + genre: 'Documentary', + categories: ['Documentary', 'Bitcoin'], + deliveryMode: 'native', + }, + { + id: 'tdf-bitcoin-end-of-money', + title: 'Bitcoin: The End of Money as We Know It', + synopsis: + 'Tracing the history of money from barter to Bitcoin.', + poster: '/images/films/posters/topdoc/bitcoin-end-of-money.jpg', + streamingUrl: 'https://www.youtube.com/embed/zpNlG3VtcBM', + genre: 'Documentary', + categories: ['Documentary', 'Bitcoin', 'Economics'], + deliveryMode: 'native', + }, + { + id: 'tdf-bitcoin-beyond-bubble', + title: 'Bitcoin: Beyond the Bubble', + synopsis: + 'An accessible explainer tracing currency evolution.', + poster: '/images/films/posters/topdoc/bitcoin-beyond-bubble.jpg', + streamingUrl: 'https://www.youtube.com/embed/URrmfEu0cZ8', + genre: 'Documentary', + categories: ['Documentary', 'Bitcoin', 'Economics'], + deliveryMode: 'native', + }, + { + id: 'tdf-bitcoin-gospel', + title: 'The Bitcoin Gospel', + synopsis: + 'The true believers argue Bitcoin is a gamechanger for the global economy.', + poster: '/images/films/posters/topdoc/bitcoin-gospel.jpg', + streamingUrl: 'https://www.youtube.com/embed/2I6dXRK9oJo', + genre: 'Documentary', + categories: ['Documentary', 'Bitcoin'], + deliveryMode: 'native', + }, + { + id: 'tdf-banking-on-bitcoin', + title: 'Banking on Bitcoin', + synopsis: + 'Chronicles idealists and entrepreneurs as they redefine money.', + poster: '/images/films/posters/topdoc/banking-on-bitcoin.jpg', + streamingUrl: 'https://www.youtube.com/embed/BbMT1Mhv7OQ', + genre: 'Documentary', + categories: ['Documentary', 'Bitcoin', 'Finance'], + deliveryMode: 'native', + }, +]; + +async function seed() { + console.log('[seed] Connecting to database...'); + await client.connect(); + + try { + // Run inside a transaction + await client.query('BEGIN'); + + // 1. Seed genres + console.log('[seed] Seeding genres...'); + for (const genre of genres) { + await client.query( + `INSERT INTO genres (id, name, type, created_at, updated_at) + VALUES ($1, $2, $3, NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name`, + [genre.id, genre.name, genre.type], + ); + } + + // Build genre lookup + const genreLookup: Record = {}; + const genreRows = await client.query('SELECT id, name FROM genres'); + for (const row of genreRows.rows) { + genreLookup[row.name] = row.id; + } + + // 2. Seed test users + console.log('[seed] Seeding test users...'); + for (const user of testUsers) { + await client.query( + `INSERT INTO users (id, email, legal_name, nostr_pubkey, created_at, updated_at) + VALUES ($1, $2, $3, $4, NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET + email = EXCLUDED.email, + nostr_pubkey = EXCLUDED.nostr_pubkey`, + [user.id, user.email, user.legalName, user.nostrPubkey], + ); + } + + // 3. Seed subscriptions for test users (cinephile plan) + console.log('[seed] Seeding subscriptions...'); + for (const user of testUsers) { + const subId = randomUUID(); + const periodEnd = new Date(); + periodEnd.setFullYear(periodEnd.getFullYear() + 1); + + await client.query( + `INSERT INTO subscriptions (id, user_id, type, period, status, period_end, created_at) + VALUES ($1, $2, 'cinephile', 'yearly', 'succeeded', $3, NOW()) + ON CONFLICT DO NOTHING`, + [subId, user.id, periodEnd], + ); + } + + // 4. Seed IndeeHub films + console.log('[seed] Seeding IndeeHub films...'); + for (const film of indeeHubFilms) { + const genreId = genreLookup[film.genre] || null; + await client.query( + `INSERT INTO projects (id, name, title, slug, synopsis, poster, status, type, genre_id, delivery_mode, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, 'published', 'film', $7, $8, NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + synopsis = EXCLUDED.synopsis, + poster = EXCLUDED.poster, + delivery_mode = EXCLUDED.delivery_mode`, + [ + film.id, + film.title, + film.title, + film.id, + film.synopsis, + film.poster, + genreId, + film.deliveryMode, + ], + ); + + // Create a content record for the film + const contentId = `content-${film.id}`; + await client.query( + `INSERT INTO contents (id, project_id, title, synopsis, status, "order", release_date, created_at, updated_at) + VALUES ($1, $2, $3, $4, 'ready', 1, NOW(), NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title`, + [contentId, film.id, film.title, film.synopsis], + ); + } + + // 5. Seed TopDoc films + console.log('[seed] Seeding TopDoc films...'); + for (const film of topDocFilms) { + const genreId = genreLookup[film.genre] || null; + await client.query( + `INSERT INTO projects (id, name, title, slug, synopsis, poster, status, type, genre_id, delivery_mode, streaming_url, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, 'published', 'film', $7, $8, $9, NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + synopsis = EXCLUDED.synopsis, + poster = EXCLUDED.poster, + delivery_mode = EXCLUDED.delivery_mode, + streaming_url = EXCLUDED.streaming_url`, + [ + film.id, + film.title, + film.title, + film.id, + film.synopsis, + film.poster, + genreId, + film.deliveryMode, + film.streamingUrl, + ], + ); + + const contentId = `content-${film.id}`; + await client.query( + `INSERT INTO contents (id, project_id, title, synopsis, status, "order", release_date, created_at, updated_at) + VALUES ($1, $2, $3, $4, 'ready', 1, NOW(), NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title`, + [contentId, film.id, film.title, film.synopsis], + ); + } + + await client.query('COMMIT'); + console.log('[seed] Database seeded successfully!'); + console.log(` - ${genres.length} genres`); + console.log(` - ${testUsers.length} test users with subscriptions`); + console.log(` - ${indeeHubFilms.length} IndeeHub films`); + console.log(` - ${topDocFilms.length} TopDoc films`); + } catch (error) { + await client.query('ROLLBACK'); + console.error('[seed] Error seeding database:', error); + process.exit(1); + } finally { + await client.end(); + } +} + +seed().catch(console.error); diff --git a/backend/src/season/dto/request/create-season-rent.entity.ts b/backend/src/season/dto/request/create-season-rent.entity.ts new file mode 100644 index 0000000..d3fc260 --- /dev/null +++ b/backend/src/season/dto/request/create-season-rent.entity.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class CreateSeasonRentDto { + @IsString() + id: string; // Season id +} diff --git a/backend/src/season/dto/request/create-season.dto.entity.ts b/backend/src/season/dto/request/create-season.dto.entity.ts new file mode 100644 index 0000000..184b5e0 --- /dev/null +++ b/backend/src/season/dto/request/create-season.dto.entity.ts @@ -0,0 +1,25 @@ +import { IsString, IsNumber, IsOptional, IsBoolean } from 'class-validator'; + +export class CreateSeasonDto { + @IsString() + projectId: string; + + @IsNumber() + seasonNumber: number; + + @IsOptional() + @IsNumber() + rentalPrice?: number; + + @IsOptional() + @IsString() + title?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/backend/src/season/dto/request/update-season.dto.entity.ts b/backend/src/season/dto/request/update-season.dto.entity.ts new file mode 100644 index 0000000..cd1f922 --- /dev/null +++ b/backend/src/season/dto/request/update-season.dto.entity.ts @@ -0,0 +1,8 @@ +import { IsOptional, IsUUID } from 'class-validator'; +import { CreateSeasonDto } from './create-season.dto.entity'; + +export class UpdateSeasonDto extends CreateSeasonDto { + @IsUUID() + @IsOptional() + id?: string; +} diff --git a/backend/src/season/dto/response/season-rent.dto.ts b/backend/src/season/dto/response/season-rent.dto.ts new file mode 100644 index 0000000..deb04ff --- /dev/null +++ b/backend/src/season/dto/response/season-rent.dto.ts @@ -0,0 +1,40 @@ +import { ProjectDTO } from 'src/projects/dto/response/project.dto'; +import { SeasonRent } from 'src/season/entities/season-rents.entity'; +import { SeasonDTO } from './season.dto'; +import { ContentDTO } from 'src/contents/dto/response/content.dto'; + +export class SeasonRentDTO { + id: string; + status: 'active' | 'expired'; + usdAmount: number; + createdAt: Date; + expiresAt: Date; + title: string; + project: ProjectDTO; + season: SeasonDTO; + contents: ContentDTO[]; + + constructor(seasonRent: SeasonRent) { + this.id = seasonRent.id; + const expiresAt = new Date(seasonRent.createdAt); + expiresAt.setDate(expiresAt.getDate() + 2); + this.status = + seasonRent.status == 'paid' && new Date() < expiresAt + ? 'active' + : 'expired'; + this.usdAmount = seasonRent.usdAmount; + this.project = seasonRent?.season?.project + ? new ProjectDTO(seasonRent.season.project) + : undefined; + this.title = + seasonRent.season?.title || seasonRent.season?.project?.title || ''; + this.createdAt = seasonRent.createdAt; + this.expiresAt = expiresAt; + this.contents = + seasonRent.season?.contents?.map((content) => new ContentDTO(content)) || + []; + this.season = seasonRent?.season + ? new SeasonDTO(seasonRent.season) + : undefined; + } +} diff --git a/backend/src/season/dto/response/season.dto.ts b/backend/src/season/dto/response/season.dto.ts new file mode 100644 index 0000000..316a506 --- /dev/null +++ b/backend/src/season/dto/response/season.dto.ts @@ -0,0 +1,29 @@ +import { ContentDTO } from 'src/contents/dto/response/content.dto'; +import { Season } from 'src/season/entities/season.entity'; + +export class SeasonDTO { + id: string; + projectId: string; + seasonNumber: number; + rentalPrice: number; + title?: string; + description?: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + contents?: ContentDTO[]; + + constructor(season: Season) { + this.id = season.id; + this.projectId = season.projectId; + this.seasonNumber = season.seasonNumber; + this.rentalPrice = season.rentalPrice; + this.title = season.title; + this.description = season.description; + this.isActive = season.isActive; + this.createdAt = season.createdAt; + this.updatedAt = season.updatedAt; + this.contents = + season?.contents?.map((content) => new ContentDTO(content)) || []; + } +} diff --git a/backend/src/season/entities/season-rents.entity.ts b/backend/src/season/entities/season-rents.entity.ts new file mode 100644 index 0000000..5c8b8ee --- /dev/null +++ b/backend/src/season/entities/season-rents.entity.ts @@ -0,0 +1,49 @@ +import { ColumnNumericTransformer } from 'src/database/transformers/column-numeric-transformer'; +import { Season } from './season.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + Index, + PrimaryColumn, +} from 'typeorm'; +import { User } from 'src/users/entities/user.entity'; + +@Entity('season_rents') +@Index(['seasonId', 'userId'], { unique: false }) +export class SeasonRent { + @PrimaryColumn() + id: string; + + @Column() + seasonId: string; + + @Column() + userId: string; + + @Column('decimal', { + precision: 5, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + usdAmount: number; + + @Column() + status: 'pending' | 'paid' | 'cancelled'; + + @Column({ nullable: true }) + providerId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @ManyToOne(() => Season, (season) => season.seasonRents) + @JoinColumn({ name: 'season_id' }) + season: Season; + + @ManyToOne(() => User, (user) => user.seasonRents) + @JoinColumn({ name: 'user_id' }) + user: User; +} diff --git a/backend/src/season/entities/season.entity.ts b/backend/src/season/entities/season.entity.ts new file mode 100644 index 0000000..ed9edb0 --- /dev/null +++ b/backend/src/season/entities/season.entity.ts @@ -0,0 +1,69 @@ +import { Project } from 'src/projects/entities/project.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + Index, + UpdateDateColumn, + OneToMany, + PrimaryColumn, +} from 'typeorm'; +import { SeasonRent } from './season-rents.entity'; +import { Content } from 'src/contents/entities/content.entity'; +import { Discount } from 'src/discounts/entities/discount.entity'; + +@Entity('seasons') +@Index(['projectId', 'seasonNumber'], { unique: true }) // Ensure unique season per project +export class Season { + @PrimaryColumn() + id: string; + + @Column({ name: 'project_id' }) + projectId: string; + + @Column() + seasonNumber: number; + + @Column('decimal', { + precision: 10, + scale: 2, + default: 0, + transformer: { + to: (value) => value, + from: (value) => Number.parseFloat(value), + }, + }) + rentalPrice: number; + + @Column({ nullable: true }) + title: string; + + @Column({ nullable: true }) + description: string; + + @Column({ default: true }) + isActive: boolean; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @ManyToOne(() => Project, (project) => project.seasons) + @JoinColumn({ name: 'project_id' }) + project: Project; + + @OneToMany(() => SeasonRent, (seasonRent) => seasonRent.season) + seasonRents: SeasonRent[]; + + @OneToMany(() => Content, (content) => content.seriesSeason) + contents: Content[]; + + @OneToMany(() => Discount, (discount) => discount.season) + discounts: Discount[]; +} + +export const fullRelations = ['project', 'seasonRents', 'contents']; diff --git a/backend/src/season/season-rents.controller.ts b/backend/src/season/season-rents.controller.ts new file mode 100644 index 0000000..4e361c8 --- /dev/null +++ b/backend/src/season/season-rents.controller.ts @@ -0,0 +1,100 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { SeasonRentsService } from './season-rents.service'; +import { SeasonRentDTO } from './dto/response/season-rent.dto'; +import { ForSeasonRentalGuard } from 'src/rents/guards/for-season-rental.guard'; +import { SeasonRentRequestDTO } from 'src/rents/dto/request/season-rent.dto'; +import { ListRentsDTO } from 'src/rents/dto/request/list-films.dto'; +import { CouponCodeDto } from 'src/discounts/dto/request/coupon-code.dto'; + +@Controller('rents/seasons') +export class SeasonRentsController { + constructor(private readonly seasonRentsService: SeasonRentsService) {} + + @Get() + @UseGuards(HybridAuthGuard) + async find(@User() user: RequestUser['user'], @Query() query: ListRentsDTO) { + const rents = await this.seasonRentsService.findByUserId(user.id, query); + return rents.map((rent) => new SeasonRentDTO(rent)); + } + + @Get('/count') + @UseGuards(HybridAuthGuard) + async findCount( + @User() user: RequestUser['user'], + @Query() query: ListRentsDTO, + ) { + const count = await this.seasonRentsService.getCountByUserId( + user.id, + query, + ); + return { count }; + } + + @Get('/project/:projectId/season/:seasonNumber') + @UseGuards(HybridAuthGuard) + async findFilm( + @Param('projectId') projectId: string, + @Param('seasonNumber') seasonNumber: number, + @User() user: RequestUser['user'], + ) { + const season = await this.seasonRentsService.findSeasonRentsByProjectId( + user.id, + projectId, + seasonNumber, + ); + + return new SeasonRentDTO(season); + } + + @Get('/content/:id/exists') + @UseGuards(HybridAuthGuard) + async filmRentExists( + @Param('id') contentId: string, + @User() { id }: RequestUser['user'], + ) { + try { + const exists = await this.seasonRentsService.seasonRentByUserExists( + id, + contentId, + ); + return { exists }; + } catch { + return { exists: false }; + } + } + + @Post('lightning') + @UseGuards(HybridAuthGuard, ForSeasonRentalGuard) + async seasonLightningRent( + @Body() { id }: SeasonRentRequestDTO, + @User() user: RequestUser['user'], + @Body() { couponCode }: CouponCodeDto, + ) { + const quote = await this.seasonRentsService.createLightningInvoice( + id, + user.email, + user.id, + couponCode, + ); + return quote; + } + + @Patch('/lightning/:id/quote') + @UseGuards(HybridAuthGuard) + async createQuote(@Param('id') invoiceId: string) { + const quote = await this.seasonRentsService.createLightningQuote(invoiceId); + return quote; + } +} diff --git a/backend/src/season/season-rents.service.ts b/backend/src/season/season-rents.service.ts new file mode 100644 index 0000000..8cc24e6 --- /dev/null +++ b/backend/src/season/season-rents.service.ts @@ -0,0 +1,309 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IsNull, Repository } from 'typeorm'; +import { SeasonRent } from './entities/season-rents.entity'; +import { SeasonService } from './season.service'; +import { BTCPayService } from 'src/payment/providers/services/btcpay.service'; +import { randomUUID } from 'node:crypto'; +import { fullRelations, Season } from './entities/season.entity'; +import { UsersService } from 'src/users/users.service'; +import { REVENUE_PERCENTAGE_TO_PAY } from 'src/rents/config/constants'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { RentsGateway } from 'src/events/rents.gateway'; +import Invoice from 'src/payment/providers/dto/strike/invoice'; +import { ListRentsDTO } from 'src/rents/dto/request/list-films.dto'; +import { Discount } from 'src/discounts/entities/discount.entity'; +import { DiscountRedemptionService } from 'src/discount-redemption/discount-redemption.service'; +import { isValidDiscountedPrice } from 'src/discounts/utils/is-valid-discount'; +import { calculateDiscountedPrice } from 'src/discounts/utils/calculate-discount-price'; + +@Injectable() +export class SeasonRentsService { + constructor( + @InjectRepository(SeasonRent) + private seasonRentRepository: Repository, + @InjectRepository(Discount) + private discountRepository: Repository, + @InjectRepository(Shareholder) + private shareholdersRepository: Repository, + @Inject(SeasonService) + private seasonService: SeasonService, + @Inject(BTCPayService) + private readonly btcpayService: BTCPayService, + @Inject(UsersService) + private readonly usersService: UsersService, + @Inject(DiscountRedemptionService) + private readonly discountRedemptionService: DiscountRedemptionService, + @Inject(RentsGateway) + private readonly eventsGateway: RentsGateway, + ) {} + + async findByUserId(userId: string, query: ListRentsDTO) { + const rentsQuery = await this.getRentsQuery(userId, query); + rentsQuery.limit(query.limit); + rentsQuery.offset(query.offset); + return rentsQuery.getMany(); + } + + async getRentsQuery(userId: string, query: ListRentsDTO) { + const rentsQuery = + this.seasonRentRepository.createQueryBuilder('seasonRent'); + rentsQuery.withDeleted(); + rentsQuery.leftJoinAndSelect('seasonRent.season', 'season'); + rentsQuery.leftJoinAndSelect('season.project', 'project'); + rentsQuery.leftJoinAndSelect('season.contents', 'contents'); + rentsQuery.where('seasonRent.userId = :userId', { userId }); + rentsQuery.andWhere('seasonRent.status = :status', { status: 'paid' }); + rentsQuery.andWhere('project.deleted_at IS NULL'); + if (query.status === 'active') { + rentsQuery.andWhere('seasonRent.createdAt > :date', { + date: this.getExpiringDate(), + }); + } else if (query.status === 'expired') { + rentsQuery.andWhere('seasonRent.createdAt < :date', { + date: this.getExpiringDate(), + }); + } + if (query?.projectId) { + rentsQuery.andWhere('project.id = :projectId', { + projectId: query.projectId, + }); + } + rentsQuery.orderBy('seasonRent.createdAt', 'DESC'); + return rentsQuery; + } + + async getCountByUserId(userId: string, query: ListRentsDTO) { + const rentsQuery = await this.getRentsQuery(userId, query); + return rentsQuery.getCount(); + } + + async findSeasonRentsByProjectId( + userId: string, + projectId: string, + seasonNumber: number, + ) { + const queryBuilder = this.seasonRentRepository + .createQueryBuilder('seasonRent') + .leftJoin('seasonRent.season', 'season') + .where('seasonRent.userId = :userId', { userId }) + .andWhere('season.projectId = :projectId', { projectId }) + .andWhere('seasonRent.status = :status', { status: 'paid' }) + .andWhere('season.seasonNumber = :seasonNumber', { seasonNumber }) + .orderBy('seasonRent.createdAt', 'DESC'); + const seasonRentByProjectId = await queryBuilder.getOne(); + if (!seasonRentByProjectId) { + throw new NotFoundException(); + } + return seasonRentByProjectId; + } + + async seasonRentByUserExists(userId: string, contentId: string) { + const count = await this.seasonRentRepository + .createQueryBuilder('seasonRent') + .innerJoin('seasonRent.season', 'season') + .innerJoin('season.contents', 'content') + .where('content.id = :contentId', { contentId }) + .andWhere('seasonRent.userId = :userId', { userId }) + .andWhere('seasonRent.status = :status', { status: 'paid' }) + .andWhere('seasonRent.createdAt >= :fromDate', { + fromDate: this.getExpiringDate(), + }) + .getCount(); + + return count > 0; + } + + async createLightningInvoice( + seasonId: string, + userEmail: string, + userId: string, + couponCode?: string, + ) { + const season = await this.seasonService.findOne(seasonId); + + const { + finalPrice, + discount, + rent: freeRental, + } = await this.processDiscount(userId, season, couponCode, userEmail); + + if (discount?.type === 'free') { + return { + ...freeRental, + }; + } + + const invoice = await this.btcpayService.issueInvoice( + finalPrice, + 'Invoice for order', + randomUUID(), + ); + const rent = await this.seasonRentRepository.save({ + id: randomUUID(), + seasonId, + userId, + usdAmount: finalPrice, + status: 'pending', + providerId: invoice.invoiceId, + }); + const quote = await this.createLightningQuote(invoice.invoiceId); + return { + ...rent, + sourceAmount: quote.sourceAmount, + conversionRate: quote.conversionRate, + expiration: quote.expiration, + lnInvoice: quote.lnInvoice, + }; + } + + async createLightningQuote(invoiceId: string) { + return this.btcpayService.issueQuote(invoiceId); + } + + async lightningPaid(invoiceId: string, invoice?: Invoice) { + if (!invoice) { + invoice = await this.btcpayService.getInvoice(invoiceId); + } + const rent = await this.seasonRentRepository.findOne({ + where: { providerId: invoiceId }, + }); + + const season = await this.seasonService.findOne( + rent.seasonId, + fullRelations, + ); + + if (!rent) { + throw new BadRequestException('Rent not found'); + } + if (invoice.state === 'PAID') { + rent.status = 'paid'; + await this.seasonRentRepository.save(rent); + await this.payShareholders(season, rent.usdAmount); + this.eventsGateway.server.emit(`${rent.id}`, { + invoiceId, + rentId: rent.id, + }); + } else { + throw new BadRequestException('Invoice not paid'); + } + } + + async payShareholders(season: Season, amount: number) { + const total = amount * REVENUE_PERCENTAGE_TO_PAY; + const contentCount = season.contents.length; + const contentRevenue = total / contentCount; + + for (const content of season.contents) { + await this.shareholdersRepository.update( + { + contentId: content.id, + deletedAt: IsNull(), + }, + { + rentPendingRevenue: () => + `rent_pending_revenue + (cast(share as decimal) / 100.00 * ${contentRevenue})`, + }, + ); + } + } + + private getExpiringDate() { + return new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); + } + + async processDiscount( + userId: string, + season: Season, + couponCode?: string, + email?: string, + ) { + let finalPrice = season.rentalPrice; + let rent: SeasonRent | undefined; + if (couponCode) { + const discount = await this.discountRepository.findOne({ + where: { + couponCode, + seasonId: season.id, + deletedAt: IsNull(), + }, + }); + + const discountedPrice = calculateDiscountedPrice( + discount.type, + discount.value, + season.rentalPrice, + ); + + if (!isValidDiscountedPrice(discountedPrice, discount.type)) { + throw new UnprocessableEntityException('Invalid discount'); + } + finalPrice = discountedPrice; + + const canRedeem = await this.discountRedemptionService.canRedeem( + discount.createdById, + discount.type, + { seasonId: season.id }, + ); + + if (!canRedeem) { + let errorMessage = + 'Maximum free redemptions reached for this filmmaker'; + if (discount?.email !== email) { + errorMessage = 'Invalid discount'; + } + this.eventsGateway.server.emit('error', { + errorMessage, + }); + throw new UnprocessableEntityException(errorMessage); + } + + if (discount.type === 'free') { + rent = await this.seasonRentRepository.save({ + id: randomUUID(), + seasonId: season.id, + userId, + usdAmount: finalPrice, + status: 'paid', + providerId: undefined, + }); + + this.eventsGateway.server.emit('free', { + invoiceId: rent.id, + rentId: rent.id, + }); + } + + await this.discountRedemptionService.createRedemption({ + userId, + seasonId: season.id, + usdAmount: finalPrice, + couponCode, + discount: { + id: discount.id, + type: discount.type, + createdById: discount.createdById, + }, + }); + + return { + finalPrice, + discount, + rent, + }; + } else { + return { + finalPrice, + discount: undefined, + rent: undefined, + }; + } + } +} diff --git a/backend/src/season/season.controller.ts b/backend/src/season/season.controller.ts new file mode 100644 index 0000000..2122333 --- /dev/null +++ b/backend/src/season/season.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { SeasonService } from './season.service'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { ListRentsDTO } from 'src/rents/dto/request/list-films.dto'; +import { SeasonRentDTO } from './dto/response/season-rent.dto'; +import { SeasonDTO } from './dto/response/season.dto'; + +@Controller('seasons') +export class SeasonController { + constructor(private readonly seasonService: SeasonService) {} + + @Get() + @UseGuards(HybridAuthGuard) + async findByUser( + @User() user: RequestUser['user'], + @Query() query: ListRentsDTO, + ) { + const rents = await this.seasonService.findSeasonRentsByUserId( + user.id, + query, + ); + return rents.map((rent) => new SeasonRentDTO(rent)); + } + + @Get(':id') + async findById(@Param('id') id: string) { + const season = await this.seasonService.findOne(id, ['contents']); + return new SeasonDTO(season); + } + + @Get('project/:id') + async findByProjectId(@Param('id') id: string) { + const seasons = await this.seasonService.findByProjectId(id); + return seasons.map((season) => new SeasonDTO(season)); + } +} diff --git a/backend/src/season/season.module.ts b/backend/src/season/season.module.ts new file mode 100644 index 0000000..5dca805 --- /dev/null +++ b/backend/src/season/season.module.ts @@ -0,0 +1,36 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SeasonService } from './season.service'; +import { Season } from './entities/season.entity'; +import { SeasonRent } from './entities/season-rents.entity'; +import { SeasonController } from './season.controller'; +import { SeasonRentsService } from './season-rents.service'; +import { SeasonRentsController } from './season-rents.controller'; +import { Shareholder } from 'src/contents/entities/shareholder.entity'; +import { RentsGateway } from 'src/events/rents.gateway'; +import { EventsModule } from 'src/events/events.module'; +import { Discount } from 'src/discounts/entities/discount.entity'; +import { DiscountRedemptionService } from 'src/discount-redemption/discount-redemption.service'; +import { DiscountRedemption } from 'src/discount-redemption/entities/discount-redemption.entity'; + +@Module({ + controllers: [SeasonController, SeasonRentsController], + imports: [ + TypeOrmModule.forFeature([ + Shareholder, + Season, + SeasonRent, + Discount, + DiscountRedemption, + ]), + EventsModule, + ], + providers: [ + SeasonService, + SeasonRentsService, + DiscountRedemptionService, + RentsGateway, + ], + exports: [SeasonService, SeasonRentsService], +}) +export class SeasonModule {} diff --git a/backend/src/season/season.service.ts b/backend/src/season/season.service.ts new file mode 100644 index 0000000..4d75834 --- /dev/null +++ b/backend/src/season/season.service.ts @@ -0,0 +1,285 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Season } from './entities/season.entity'; +import { In, Repository } from 'typeorm'; +import { Content } from 'src/contents/entities/content.entity'; +import { SeasonRent } from './entities/season-rents.entity'; +import { ListRentsDTO } from 'src/rents/dto/request/list-films.dto'; +import { randomUUID } from 'node:crypto'; +import { UpdateSeasonDto } from './dto/request/update-season.dto.entity'; + +@Injectable() +export class SeasonService { + constructor( + @InjectRepository(Season) + private seasonRepository: Repository, + @InjectRepository(SeasonRent) + private seasonRentRepository: Repository, + ) {} + + findOne(id: string, relations?: string[]) { + return this.seasonRepository.findOne({ + where: { id }, + relations, + }); + } + + async findSeasonsByProjectAndNumbers( + projectId: string, + seasonNumbers: number[], + ) { + const existingSeasons = await this.seasonRepository.find({ + where: { + projectId, + seasonNumber: In(seasonNumbers), + }, + }); + + return existingSeasons; + } + + async findSeasonRentsByUserId(userId: string, query: ListRentsDTO) { + const rentsQuery = await this.getRentsQuery(userId, query); + rentsQuery.limit(query.limit); + rentsQuery.offset(query.offset); + return rentsQuery.getMany(); + } + + async findByProjectId(projectId: string) { + return this.seasonRepository.find({ + where: { projectId }, + }); + } + + async getOrCreateSeasonsUpsert( + projectId: string, + seasonsToUpdate: UpdateSeasonDto[], + ): Promise { + const seasonNumbers = seasonsToUpdate.map((s) => s.seasonNumber); + + if (seasonNumbers.length === 0) { + return []; + } + + const uniqueSeasonNumbers = [...new Set(seasonNumbers)].sort( + (a, b) => a - b, + ); + + const existingSeasons = await this.findSeasonsByProjectAndNumbers( + projectId, + uniqueSeasonNumbers, + ); + + // Create a map for quick lookup of existing season IDs + const existingSeasonsMap = new Map( + existingSeasons.map((season) => [season.seasonNumber, season.id]), + ); + + const existingSeasonNumbers = new Set( + existingSeasons.map((s) => s.seasonNumber), + ); + + const newSeasonNumbers = new Set( + uniqueSeasonNumbers.filter( + (number_) => !existingSeasonNumbers.has(number_), + ), + ); + + // Create upsert data + const seasonsData = uniqueSeasonNumbers.map((seasonNumber) => ({ + id: newSeasonNumbers.has(seasonNumber) + ? randomUUID() + : existingSeasonsMap.get(seasonNumber), + projectId, + seasonNumber, + rentalPrice: + seasonsToUpdate.find( + (seasonToUpdate) => seasonToUpdate.seasonNumber === seasonNumber, + )?.rentalPrice || 0, + title: `Season ${seasonNumber}`, + })); + + // Upsert all seasons at once + await this.seasonRepository.upsert(seasonsData, { + conflictPaths: ['projectId', 'seasonNumber'], // Composite unique constraint + skipUpdateIfNoValuesChanged: true, + }); + + // Return all seasons for the project + return this.seasonRepository.find({ + where: { + projectId, + seasonNumber: In(uniqueSeasonNumbers), + }, + }); + } + + async removeByProjectId(projectId: string, seasonNumbers?: number[]) { + if (seasonNumbers && seasonNumbers.length === 0) { + return await this.seasonRepository.delete({ + projectId, + seasonNumber: In(seasonNumbers), + }); + } + await this.seasonRepository.delete({ + projectId, + }); + } + + async bulkUpdateSeasons( + updates: { + projectId: string; + seasonNumber: number; + rentalPrice?: number; + title?: string; + description?: string; + isActive?: boolean; + }[], + ): Promise { + await this.seasonRepository.manager.transaction(async (manager) => { + for (const update of updates) { + const { + projectId, + seasonNumber, + rentalPrice, + title, + description, + isActive, + } = update; + + await manager + .getRepository(Season) + .createQueryBuilder() + .update(Season) + .set({ + rentalPrice, + title, + description, + isActive, + }) + .where('project_id = :projectId AND season_number = :seasonNumber', { + projectId, + seasonNumber, + }) + .execute(); + } + }); + } + + /** + * Based on a comparison of the current seasons and the new contents, + * determines which seasons already exist, which have been removed, + * and which should be added. + * + * @param projectId - The ID of the project to which the seasons belong. + * @param newContents - List of content items, each with a `season` property (season number). + * @param currentSeasons - Currently registered seasons associated with the project. + * @returns An object with three arrays: + * - `addedSeasons`: seasons found in the new contents but not in the current list. + * - `existingSeasons`: seasons present in both current and new lists. + * - `removedSeasons`: seasons that are no longer present in the new list. + */ + analizeSeasonDiff( + projectId: string, + newContents: Pick[], + currentSeasons: Pick[], + ) { + const newSeasons = newContents.map((newContent) => ({ + projectId, + seasonNumber: newContent.season, + })); + + const { existing, removed, added } = this.compareSeasons( + currentSeasons, + newSeasons, + ); + + return { + added, + existing, + removed, + }; + } + + /** + * Determines which seasons already exist, which have been removed, + * and which should be added. It does not verify if the season has been updated. + * + * @param projectId - The ID of the project to which the seasons belong. + * @param newContents - List of content items, each with a `season` property (season number). + * @param currentSeasons - Currently registered seasons associated with the project. + * @returns An object with three arrays: + * - `addedSeasons`: seasons found in the new contents but not in the current list. + * - `existingSeasons`: seasons present in both current and new lists. + * - `removedSeasons`: seasons that are no longer present in the new list. + */ + compareSeasons( + currentSeasons: Pick[], + newSeasons: Pick[], + ) { + const createKey = ( + season: Pick, + ): string => `${season.projectId}-${season.seasonNumber}`; + + const currentSeasonsMap = new Map( + currentSeasons.map((season) => [createKey(season), season]), + ); + const newSeasonsMap = new Map( + newSeasons.map((season) => [createKey(season), season]), + ); + + const existing: Pick[] = []; + const removed: Pick[] = []; + const added: Pick[] = []; + + const allKeys = new Set([ + ...currentSeasonsMap.keys(), + ...newSeasonsMap.keys(), + ]); + + for (const key of allKeys) { + const currentSeason = currentSeasonsMap.get(key); + const newSeason = newSeasonsMap.get(key); + + if (currentSeason && newSeason) { + existing.push(currentSeason); + } else if (currentSeason && !newSeason) { + removed.push(currentSeason); + } else if (!currentSeason && newSeason) { + added.push(newSeason); + } + } + + return { + existing, + removed, + added, + }; + } + + async getRentsQuery(userId: string, query: ListRentsDTO) { + const rentsQuery = + this.seasonRentRepository.createQueryBuilder('season_rent'); + rentsQuery.withDeleted(); + rentsQuery.leftJoinAndSelect('season_rent.content', 'content'); + rentsQuery.leftJoinAndSelect('content.project', 'project'); + rentsQuery.where('season_rent.userId = :userId', { userId }); + rentsQuery.andWhere('season_rent.status = :status', { status: 'paid' }); + rentsQuery.andWhere('project.deleted_at IS NULL'); + if (query.status === 'active') { + rentsQuery.andWhere('season_rent.createdAt > :date', { + date: this.getExpiringDate(), + }); + } else if (query.status === 'expired') { + rentsQuery.andWhere('season_rent.createdAt < :date', { + date: this.getExpiringDate(), + }); + } + rentsQuery.orderBy('season_rent.createdAt', 'DESC'); + return rentsQuery; + } + + private getExpiringDate() { + return new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); + } +} diff --git a/backend/src/secrets-manager/secrets-manager.module.ts b/backend/src/secrets-manager/secrets-manager.module.ts new file mode 100644 index 0000000..1175f68 --- /dev/null +++ b/backend/src/secrets-manager/secrets-manager.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { SecretsManagerService } from './secrets-manager.service'; +import { ConfigModule } from '@nestjs/config'; + +/** + * Secrets manager module. + * Replaced AWS Secrets Manager with environment variable-based secrets. + * Secrets are stored in .env or Docker secrets in production. + */ +@Module({ + imports: [ConfigModule], + providers: [SecretsManagerService], + exports: [SecretsManagerService], +}) +export class SecretsManagerModule {} diff --git a/backend/src/secrets-manager/secrets-manager.service.ts b/backend/src/secrets-manager/secrets-manager.service.ts new file mode 100644 index 0000000..2fd2ed5 --- /dev/null +++ b/backend/src/secrets-manager/secrets-manager.service.ts @@ -0,0 +1,36 @@ +import { Injectable, Logger } from '@nestjs/common'; + +/** + * Secrets manager service. + * Replaced AWS Secrets Manager with environment variable-based secrets. + * Secrets are read from environment variables prefixed with SECRET_. + */ +@Injectable() +export class SecretsManagerService { + private readonly logger = new Logger(SecretsManagerService.name); + + async getSecret(secretName: string): Promise { + try { + // Try to read from environment variable + // Converts secret name like "my/secret/name" to "SECRET_MY_SECRET_NAME" + const envKey = `SECRET_${secretName.replace(/[/-]/g, '_').toUpperCase()}`; + const value = process.env[envKey]; + + if (value) { + try { + return JSON.parse(value); + } catch { + return value; + } + } + + this.logger.warn( + `Secret "${secretName}" not found. Set env var "${envKey}" to provide it.`, + ); + return undefined; + } catch { + this.logger.error('Error while fetching secret from environment'); + return undefined; + } + } +} diff --git a/backend/src/subscriptions/decorators/subscriptions.decorator.ts b/backend/src/subscriptions/decorators/subscriptions.decorator.ts new file mode 100644 index 0000000..3553ae3 --- /dev/null +++ b/backend/src/subscriptions/decorators/subscriptions.decorator.ts @@ -0,0 +1,5 @@ +import { Reflector } from '@nestjs/core'; +import { AllowedSubscriptionType } from '../enums/types.enum'; + +export const Subscriptions = + Reflector.createDecorator(); diff --git a/backend/src/subscriptions/dto/admin-create-subscription.dto.ts b/backend/src/subscriptions/dto/admin-create-subscription.dto.ts new file mode 100644 index 0000000..70c6935 --- /dev/null +++ b/backend/src/subscriptions/dto/admin-create-subscription.dto.ts @@ -0,0 +1,11 @@ +import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { CreateSubscriptionDTO } from './create-subscription.dto'; + +export class AdminCreateSubscriptionDTO extends CreateSubscriptionDTO { + @IsEnum(['once', 'forever']) + duration: 'once' | 'forever'; + + @IsOptional() + @IsString() + userId: string; +} diff --git a/backend/src/subscriptions/dto/create-subscription.dto.ts b/backend/src/subscriptions/dto/create-subscription.dto.ts new file mode 100644 index 0000000..7bf27c2 --- /dev/null +++ b/backend/src/subscriptions/dto/create-subscription.dto.ts @@ -0,0 +1,11 @@ +import { IsEnum } from 'class-validator'; +import { SubscriptionType, subscriptionTypes } from '../enums/types.enum'; +import { SubscriptionPeriod, subscriptionPeriods } from '../enums/periods.enum'; + +export class CreateSubscriptionDTO { + @IsEnum(subscriptionPeriods) + period: SubscriptionPeriod; + + @IsEnum(subscriptionTypes) + type: SubscriptionType; +} diff --git a/backend/src/subscriptions/dto/flash-webhook-event.dto.ts b/backend/src/subscriptions/dto/flash-webhook-event.dto.ts new file mode 100644 index 0000000..5513ca6 --- /dev/null +++ b/backend/src/subscriptions/dto/flash-webhook-event.dto.ts @@ -0,0 +1,37 @@ +import { flashEventType } from '../enums/flash-event.enum'; +import { SubscriptionType } from '../enums/types.enum'; + +export interface FlashWebhookData { + public_key: string; + name: string; + email: string; + about?: string; + picture_url: string; + user_plan: SubscriptionType; + user_plan_id: number; + signup_date: string; + next_payment_date: string; + failed_payment_date: string; + transaction_id: string; + transaction_amount: number; + transaction_currency: string; + transaction_date: string; + external_uuid: string; +} + +export interface FlashWebhookEvent { + version: string; + eventType: { + id: number; + name: (typeof flashEventType)[number]; + }; + user_public_key: string; + exp: number; // Expiration time as a Unix timestamp + // iat: number; // Issued at time as a Unix timestamp +} + +export interface Flash { + event: FlashWebhookEvent; + data: FlashWebhookData; + type: SubscriptionType; +} diff --git a/backend/src/subscriptions/dto/request/create-billing-query.dto.ts b/backend/src/subscriptions/dto/request/create-billing-query.dto.ts new file mode 100644 index 0000000..25f487a --- /dev/null +++ b/backend/src/subscriptions/dto/request/create-billing-query.dto.ts @@ -0,0 +1,10 @@ +import { IsIn } from 'class-validator'; +import { + SubscriptionType, + subscriptionTypes, +} from 'src/subscriptions/enums/types.enum'; + +export class CreateBillingDTO { + @IsIn(subscriptionTypes) + type: SubscriptionType; +} diff --git a/backend/src/subscriptions/dto/response/subscription.dto.ts b/backend/src/subscriptions/dto/response/subscription.dto.ts new file mode 100644 index 0000000..9c019c4 --- /dev/null +++ b/backend/src/subscriptions/dto/response/subscription.dto.ts @@ -0,0 +1,29 @@ +import { Subscription } from 'src/subscriptions/entities/subscription.entity'; +import { SubscriptionPeriod } from 'src/subscriptions/enums/periods.enum'; +import { PaymentStatus } from 'src/subscriptions/enums/status.enum'; +import { SubscriptionType } from 'src/subscriptions/enums/types.enum'; + +export class SubscriptionDTO { + id: string; + stripeId: string; + status: PaymentStatus; + type: SubscriptionType; + period: SubscriptionPeriod; + periodEnd?: Date; + createdAt: Date; + flashId?: string; + flashEmail?: string; + + constructor(subscription: Subscription) { + this.id = subscription.id; + this.stripeId = subscription.stripeId; + this.status = subscription.status; + this.type = subscription.type; + this.periodEnd = subscription.periodEnd; + this.createdAt = subscription.createdAt; + this.flashId = subscription?.flashId; + this.flashEmail = subscription?.flashEvents?.length + ? subscription?.flashEvents[0]?.data?.email + : undefined; + } +} diff --git a/backend/src/subscriptions/entities/subscription.entity.ts b/backend/src/subscriptions/entities/subscription.entity.ts new file mode 100644 index 0000000..b1d2f9b --- /dev/null +++ b/backend/src/subscriptions/entities/subscription.entity.ts @@ -0,0 +1,54 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; +import { PaymentStatus } from '../enums/status.enum'; +import { SubscriptionPeriod } from '../enums/periods.enum'; +import { User } from 'src/users/entities/user.entity'; +import { SubscriptionType } from '../enums/types.enum'; +import { Flash } from '../dto/flash-webhook-event.dto'; + +@Entity('subscriptions') +export class Subscription { + @PrimaryColumn() + id: string; + + @Column({ nullable: true }) + stripeId: string; + + // Called external_uuid by pay with flash + @Column({ nullable: true }) + flashId: string; + + @Column() + status: PaymentStatus; + + @Column() + userId: string; + + @Column() + type: SubscriptionType; + + @Column() + period: SubscriptionPeriod; + + @Column({ type: 'timestamptz', nullable: true }) + periodEnd?: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: true }) + expirationReminderSentAt?: Date; + + @Column({ type: 'jsonb', nullable: true }) + flashEvents: Flash[]; + + @ManyToOne(() => User, (user) => user.subscriptions) + @JoinColumn({ name: 'user_id' }) + user?: User; +} diff --git a/backend/src/subscriptions/enums/flash-event.enum.ts b/backend/src/subscriptions/enums/flash-event.enum.ts new file mode 100644 index 0000000..53cd339 --- /dev/null +++ b/backend/src/subscriptions/enums/flash-event.enum.ts @@ -0,0 +1,7 @@ +export const flashEventType = [ + 'user_signed_up' as const, + 'renewal_successful' as const, + 'renewal_failed' as const, + 'user_cancelled_subscription' as const, +] as const; +export type FlashEventType = (typeof flashEventType)[number]; diff --git a/backend/src/subscriptions/enums/periods.enum.ts b/backend/src/subscriptions/enums/periods.enum.ts new file mode 100644 index 0000000..b685dad --- /dev/null +++ b/backend/src/subscriptions/enums/periods.enum.ts @@ -0,0 +1,2 @@ +export const subscriptionPeriods = ['yearly', 'monthly'] as const; +export type SubscriptionPeriod = (typeof subscriptionPeriods)[number]; diff --git a/backend/src/subscriptions/enums/status.enum.ts b/backend/src/subscriptions/enums/status.enum.ts new file mode 100644 index 0000000..1d8f33b --- /dev/null +++ b/backend/src/subscriptions/enums/status.enum.ts @@ -0,0 +1,10 @@ +export const paymentStatus = [ + 'created', + 'processing', + 'succeeded', + 'rejected', + 'cancelled', + 'paused', + 'expired', +] as const; +export type PaymentStatus = (typeof paymentStatus)[number]; diff --git a/backend/src/subscriptions/enums/types.enum.ts b/backend/src/subscriptions/enums/types.enum.ts new file mode 100644 index 0000000..12fc1c4 --- /dev/null +++ b/backend/src/subscriptions/enums/types.enum.ts @@ -0,0 +1,20 @@ +export const subscriptionTypes = [ + 'enthusiast', + 'film-buff', + 'cinephile', + 'rss-addon', + 'verification-addon', + // Discontinued subscriptions but still in the database: + 'audience', + 'pro-plus', + 'ultimate', +] as const; +export type SubscriptionType = (typeof subscriptionTypes)[number]; + +export const allowedSubscriptionTypes = [ + ...subscriptionTypes, + 'free', + 'pro', + 'filmmaker', +] as const; +export type AllowedSubscriptionType = (typeof allowedSubscriptionTypes)[number]; diff --git a/backend/src/subscriptions/flash-subscription.service.ts b/backend/src/subscriptions/flash-subscription.service.ts new file mode 100644 index 0000000..e8da907 --- /dev/null +++ b/backend/src/subscriptions/flash-subscription.service.ts @@ -0,0 +1,252 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Subscription } from './entities/subscription.entity'; +import { Repository } from 'typeorm'; +import { UsersService } from 'src/users/users.service'; +import { User } from 'src/users/entities/user.entity'; +import { randomUUID } from 'node:crypto'; +import { Flash } from './dto/flash-webhook-event.dto'; +import { flashEventType } from './enums/flash-event.enum'; +import { SubscriptionType } from './enums/types.enum'; +import { SubscriptionsGateway } from 'src/events/subscriptions.gateway'; +import { CreateSubscriptionDTO } from './dto/create-subscription.dto'; +import * as dayjs from 'dayjs'; +import { SubscriptionPeriod } from './enums/periods.enum'; + +const validateTXID = (id: string) => + id === '' || id === null || id === undefined || id === '-1'; + +@Injectable() +export class FlashSubscriptionsService { + constructor( + @Inject(UsersService) + private readonly usersService: UsersService, + @InjectRepository(Subscription) + private readonly subscriptionsRepository: Repository, + @Inject(SubscriptionsGateway) + private readonly subscriptionsGateway: SubscriptionsGateway, + ) {} + + async handleUserSignedUp({ data, event, type }: Flash, user: User) { + if (event.eventType.id !== 1) { + Logger.error( + `Event type invalid, expected "1" got "${event.eventType.id}"`, + ); + return; + } + + if (data.failed_payment_date !== '') { + Logger.error( + `Signup payment failed for user: ${data.email}, expected success`, + ); + return; + } + + if (validateTXID(data.transaction_id)) { + Logger.error( + `no signup payment for user: ${data.email}, expected payment`, + ); + return; + } + + if ( + user.subscriptions.some((sub) => + sub?.flashEvents?.some( + (event) => event.data.transaction_id === data.transaction_id, + ), + ) + ) { + Logger.warn( + `already received this account creation event for user ${data.email}`, + ); + return; + } + + const flashSubscriptionId = data.external_uuid; + + const subscription = await this.subscriptionsRepository.findOneBy({ + flashId: flashSubscriptionId, + userId: user.id, + }); + + if (subscription) { + subscription.periodEnd = new Date(data.next_payment_date); + subscription.type = type; + subscription.flashEvents = [{ data, event, type }]; + subscription.period = data.user_plan.toLowerCase() as SubscriptionPeriod; + subscription.status = 'succeeded'; + + await this.subscriptionsRepository.save(subscription); + + this.subscriptionsGateway.server.emit(user.id, { + subscriptionId: subscription.id, + }); + + Logger.log(`User "${data.email}" signed up successfully!`); + } + } + + async handleRenewalSuccessful({ data, event, type }: Flash, user: User) { + if (event.eventType.id !== 2) { + Logger.error( + `Event type invalid, expected "2" got "${event.eventType.id}"`, + ); + return; + } + + if (data.failed_payment_date !== '') { + Logger.error( + `renewal payment failed for user: ${data.email}, expected success`, + ); + return; + } + + if (validateTXID(data.transaction_id)) { + Logger.error( + `no renewal payment for user: ${data.email}, expected payment`, + ); + return; + } + + const subscription = await this.subscriptionsRepository.findOne({ + where: { userId: user.id }, + }); + + if (!subscription) { + Logger.error('Subscription not found for user:', data.email); + return; + } + + if ( + subscription.flashEvents.some( + (event) => event.data.transaction_id === data.transaction_id, + ) + ) { + Logger.warn('Duplicate event detected:', data.transaction_id); + return; + } + + subscription.flashEvents = subscription.flashEvents || []; + subscription.flashEvents.push({ event, data, type }); + subscription.status = 'succeeded'; + subscription.periodEnd = new Date(data.next_payment_date); + subscription.type = data.user_plan.toLowerCase() as SubscriptionType; + + await this.subscriptionsRepository.save(subscription); + Logger.log(`User "${data.email}" renewed successfully!`); + } + + async handleRenewalFailed({ event, data, type }: Flash, user: User) { + if (event.eventType.id !== 3) { + Logger.error( + `Event type invalid, expected "3" got "${event.eventType.id}"`, + ); + return; + } + + const subscription = await this.subscriptionsRepository.findOne({ + where: { userId: user.id }, + }); + + if (!subscription) { + Logger.error('Subscription not found for user:', data.email); + return; + } + + if ( + subscription.flashEvents.some( + (event) => event.data.failed_payment_date === data.failed_payment_date, + ) + ) { + Logger.warn('Duplicate failed renewal event detected:', data); + return; + } + + if (data.transaction_id !== '-1') { + Logger.error( + 'Received a failed event that was not a failed event?:', + data, + ); + return; + } + + subscription.flashEvents = subscription.flashEvents || []; + subscription.flashEvents.push({ event, data, type }); + subscription.status = 'expired'; + subscription.periodEnd = new Date(data.failed_payment_date); + + await this.subscriptionsRepository.save(subscription); + Logger.log(`User "${data.email}" either failed payment, or unsubscribed.`); + } + + async updateSubscriptionStatus(payload: Flash) { + if (!payload.event.version.startsWith('1.')) { + Logger.error( + `Can not process Flash Event version "${payload.event.version}"`, + ); + return; + } + + const flashSubscriptionId = payload.data.external_uuid; + + // Existing bug on paywithflash service makes to the request to not include the correct email + const subscription = await this.subscriptionsRepository.findOne({ + where: { + flashId: flashSubscriptionId, + }, + relations: ['user'], + }); + + const userEmail = subscription?.user.email; + const user = await this.usersService.findUserByEmail(userEmail); + + if (!user) { + Logger.error(`User not found for Flash event: "${userEmail}"`); + return; + } + + switch (payload.event.eventType.name) { + case flashEventType[0]: { + Logger.log('Handling user sign-up'); + await this.handleUserSignedUp(payload, user); + break; + } + case flashEventType[1]: { + Logger.log('Handling successful renewal'); + await this.handleRenewalSuccessful(payload, user); + break; + } + case flashEventType[2]: { + Logger.log('Handling failed renewal'); + await this.handleRenewalFailed(payload, user); + break; + } + case flashEventType[3]: { + Logger.log('Handling cancellation'); + // Flash does not support proration so periodEnd and status will remain the same + break; + } + default: { + Logger.error('Unknown event type:', JSON.stringify(payload)); + } + } + } + + async initFlashPayment({ period, type }: CreateSubscriptionDTO, user: User) { + const subscriptionFlashId = randomUUID(); + await this.subscriptionsRepository.save({ + id: randomUUID(), + flashId: subscriptionFlashId, + status: 'created', + type, + period, + userId: user.id, + periodEnd: + period === 'monthly' + ? dayjs().add(1, 'month').toDate() + : dayjs().add(1, 'year').toDate(), + }); + + return subscriptionFlashId; + } +} diff --git a/backend/src/subscriptions/guards/pay-with-flash.guard.ts b/backend/src/subscriptions/guards/pay-with-flash.guard.ts new file mode 100644 index 0000000..591e472 --- /dev/null +++ b/backend/src/subscriptions/guards/pay-with-flash.guard.ts @@ -0,0 +1,60 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import * as jwt from 'jsonwebtoken'; +import { SubscriptionType } from '../enums/types.enum'; + +@Injectable() +export class PayWithFlashAuthGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader: string | undefined = request.headers.authorization; + const token = (authHeader ?? '').split(' ')[1]; + + const { url } = request; + + // get the type from the URL + const type: SubscriptionType = url.split('/').pop() as SubscriptionType; + + if (!token) { + throw new UnauthorizedException('No token provided'); + } + + let secret: string; + switch (type) { + case 'rss-addon': { + secret = process.env.FLASH_JWT_SECRET_RSS_ADDON; + break; + } + case 'verification-addon': { + secret = process.env.FLASH_JWT_SECRET_VERIFICATION_ADDON; + break; + } + case 'enthusiast': { + secret = process.env.FLASH_JWT_SECRET_ENTHUSIAST; + break; + } + case 'film-buff': { + secret = process.env.FLASH_JWT_SECRET_FILM_BUFF; + break; + } + case 'cinephile': { + secret = process.env.FLASH_JWT_SECRET_CINEPHILE; + break; + } + default: { + throw new UnauthorizedException('Invalid subscription type'); + } + } + try { + const decoded = jwt.verify(token, secret); + request.user = decoded; // Attach user information to the request + return true; + } catch { + throw new UnauthorizedException('Invalid token'); + } + } +} diff --git a/backend/src/subscriptions/guards/subscription.guard.ts b/backend/src/subscriptions/guards/subscription.guard.ts new file mode 100644 index 0000000..cd9cff4 --- /dev/null +++ b/backend/src/subscriptions/guards/subscription.guard.ts @@ -0,0 +1,41 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Subscriptions } from '../decorators/subscriptions.decorator'; +import { User } from 'src/users/entities/user.entity'; +import { Subscription } from '../entities/subscription.entity'; + +@Injectable() +export class SubscriptionsGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const subscriptions = this.reflector.get( + Subscriptions, + context.getHandler(), + ); + if (!subscriptions) { + return true; + } + const enabledSubscriptions = new Set(subscriptions); + const request = context.switchToHttp().getRequest(); + const user: User = request.user; + + if (subscriptions.includes('filmmaker')) return !!user.filmmaker; + if (this.hasActiveSubscription(user.subscriptions)) { + return user.subscriptions.some((sub) => + enabledSubscriptions.has(sub.type), + ); + } + return true; + } + + hasActiveSubscription(subscriptions: Subscription[]) { + if (subscriptions.length === 0) return false; + const latestSubscription = subscriptions[0]; + const currentDate = new Date(); + // get subscription expiration date + const subscriptionExpirationDate = latestSubscription.periodEnd; + // check if subscription is active + return currentDate < subscriptionExpirationDate; + } +} diff --git a/backend/src/subscriptions/helpers/subscription.helper.ts b/backend/src/subscriptions/helpers/subscription.helper.ts new file mode 100644 index 0000000..e692540 --- /dev/null +++ b/backend/src/subscriptions/helpers/subscription.helper.ts @@ -0,0 +1,35 @@ +import { SubscriptionType } from '../enums/types.enum'; + +export const getBenefits = (type: SubscriptionType) => { + if (type === 'rss-addon') { + return 'the ability to publish your content through RSS and automatically reach your audience'; + } + return 'access to the Screening Room with exclusive films, series, and music videos'; +}; + +export const getPurchaseUrl = (type: SubscriptionType) => { + const frontendUrl = process.env.FRONTEND_URL || 'https://indeehub.studio'; + if (type === 'rss-addon') { + return `${frontendUrl}/add-ons`; + } + return `${frontendUrl}/subscription`; +}; + +export const getSubsctriptionName = (type: SubscriptionType) => { + const formattedType = formatSubscriptionType(type); + if (type === 'rss-addon') { + return 'RSS Add-on'; + } + return formattedType + ' Subscription'; +}; + +/** + * Capitalizes the first letter of each word and removes hyphens. + * Example: 'film-buff' => 'Film Buff' + */ +export const formatSubscriptionType = (type: SubscriptionType): string => { + return type + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; diff --git a/backend/src/subscriptions/subscription-email.service.ts b/backend/src/subscriptions/subscription-email.service.ts new file mode 100644 index 0000000..1cd64bd --- /dev/null +++ b/backend/src/subscriptions/subscription-email.service.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Subscription } from './entities/subscription.entity'; +import { Brackets, Repository } from 'typeorm'; +import { MailService } from 'src/mail/mail.service'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import * as dayjs from 'dayjs'; +import { + getBenefits, + getPurchaseUrl, + getSubsctriptionName, +} from './helpers/subscription.helper'; + +@Injectable() +export class SubscriptionsEmailService { + constructor( + @InjectRepository(Subscription) + private readonly _subscriptionsRepository: Repository, + private readonly _mailService: MailService, + ) {} + + @Cron(CronExpression.EVERY_DAY_AT_11AM) + async handleExpiringReminderCron() { + Logger.log('Looking for subscriptions expiring soon...'); + const now = dayjs(); + const in24Hours = now.add(24, 'hour').toDate(); + const last24Hours = now.subtract(24, 'hour').toDate(); + + // Query for subscriptions expiring in the next 24 hours + // and that haven't had a reminder sent in the last 24 hours + const expiring = await this._subscriptionsRepository + .createQueryBuilder('subscription') + // using withDeleted to include soft-deleted users data then exclude them later + // otherwise, the query would not join the user table correctly + .withDeleted() + .leftJoinAndSelect('subscription.user', 'user') + .where('subscription.periodEnd BETWEEN :now AND :in24Hours', { + now, + in24Hours, + }) + .andWhere('subscription.status = :status', { status: 'cancelled' }) + .andWhere( + new Brackets((qb) => { + qb.where('subscription.expirationReminderSentAt IS NULL').orWhere( + 'subscription.expirationReminderSentAt < :last24Hours', + { last24Hours }, + ); + }), + ) + .andWhere('user.deletedAt IS NULL') + .getMany(); + + if (expiring.length === 0) { + Logger.log('No subscriptions expiring soon.'); + return; + } + + // Sending reminder emails + const renewSubscriptionTemplateId = 'd-681385c5f24c412e95ad70bc6410511d'; + const recipients = expiring.map((subscription) => ({ + email: subscription.user.email, + data: { + subscriptionAddonUrl: getPurchaseUrl(subscription.type), + subscriptionName: getSubsctriptionName(subscription.type), + benefits: getBenefits(subscription.type), + }, + })); + + await this._mailService.sendBatch(renewSubscriptionTemplateId, recipients); + + // Setting the reminder sent date + const ids = expiring.map((s) => s.id); + await this._subscriptionsRepository + .createQueryBuilder() + .update() + .set({ expirationReminderSentAt: new Date() }) + .whereInIds(ids) + .execute(); + + Logger.log( + `Sent reminder emails for ${expiring.length} expiring subscriptions.`, + ); + } +} diff --git a/backend/src/subscriptions/subscriptions.controller.ts b/backend/src/subscriptions/subscriptions.controller.ts new file mode 100644 index 0000000..ff623c4 --- /dev/null +++ b/backend/src/subscriptions/subscriptions.controller.ts @@ -0,0 +1,86 @@ +import { + Controller, + Post, + Body, + UseGuards, + HttpCode, + HttpStatus, + Req, + Param, + Get, +} from '@nestjs/common'; +import { SubscriptionsService } from './subscriptions.service'; +import { CreateSubscriptionDTO } from './dto/create-subscription.dto'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { User } from 'src/auth/user.decorator'; +import { RequestUser } from 'src/auth/dto/request/request-user.interface'; +import { + FlashWebhookData, + FlashWebhookEvent, +} from './dto/flash-webhook-event.dto'; +import { FlashSubscriptionsService } from './flash-subscription.service'; +import { SubscriptionsGuard } from './guards/subscription.guard'; +import { PayWithFlashAuthGuard } from './guards/pay-with-flash.guard'; +import { SubscriptionType } from './enums/types.enum'; + +@Controller('subscriptions') +export class SubscriptionsController { + constructor( + private readonly subscriptionsService: SubscriptionsService, + private readonly flashSubscriptionsService: FlashSubscriptionsService, + ) {} + + @Post('/flash/init') + @UseGuards(HybridAuthGuard) + async initFlashPayment( + @Body() createSubscriptionDTO: CreateSubscriptionDTO, + @User() user: RequestUser['user'], + ) { + const subscriptionFlashId = + await this.flashSubscriptionsService.initFlashPayment( + createSubscriptionDTO, + user, + ); + return { subscriptionFlashId }; + } + + /** + * Create a Lightning invoice for a subscription payment. + * Returns BOLT11 invoice, expiration, and amount for QR code display. + */ + @Post('/lightning') + @UseGuards(HybridAuthGuard) + async createLightningSubscription( + @Body() createSubscriptionDTO: CreateSubscriptionDTO, + @User() user: RequestUser['user'], + ) { + return await this.subscriptionsService.createLightningSubscription( + createSubscriptionDTO, + user, + ); + } + + /** + * Get the current user's active subscriptions. + */ + @Get('/active') + @UseGuards(HybridAuthGuard) + async getActiveSubscriptions(@User() user: RequestUser['user']) { + return await this.subscriptionsService.getActiveSubscriptions(user.id); + } + + @Post('flash-payment-webhook/:type') + @UseGuards(PayWithFlashAuthGuard) + @HttpCode(HttpStatus.OK) + async flashPayment( + @Req() { user: event }: { user: FlashWebhookEvent }, + @Body() { data }: { data: FlashWebhookData }, + @Param('type') type: SubscriptionType, + ) { + await this.flashSubscriptionsService.updateSubscriptionStatus({ + event, + data, + type, + }); + } +} diff --git a/backend/src/subscriptions/subscriptions.module.ts b/backend/src/subscriptions/subscriptions.module.ts new file mode 100644 index 0000000..0ca4458 --- /dev/null +++ b/backend/src/subscriptions/subscriptions.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { SubscriptionsService } from './subscriptions.service'; +import { FlashSubscriptionsService } from './flash-subscription.service'; +import { SubscriptionsController } from './subscriptions.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Subscription } from './entities/subscription.entity'; +import { EventsModule } from 'src/events/events.module'; +import { SubscriptionsEmailService } from './subscription-email.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Subscription]), EventsModule], + controllers: [SubscriptionsController], + providers: [ + SubscriptionsService, + FlashSubscriptionsService, + SubscriptionsEmailService, + ], + exports: [SubscriptionsService, FlashSubscriptionsService], +}) +export class SubscriptionsModule {} diff --git a/backend/src/subscriptions/subscriptions.service.ts b/backend/src/subscriptions/subscriptions.service.ts new file mode 100644 index 0000000..579863b --- /dev/null +++ b/backend/src/subscriptions/subscriptions.service.ts @@ -0,0 +1,172 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { CreateSubscriptionDTO } from './dto/create-subscription.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Subscription } from './entities/subscription.entity'; +import { In, IsNull, Not, Repository } from 'typeorm'; +import { UsersService } from 'src/users/users.service'; +import { User } from 'src/users/entities/user.entity'; +import { randomUUID } from 'node:crypto'; +import { SubscriptionType } from './enums/types.enum'; +import { SubscriptionPeriod } from './enums/periods.enum'; +import { MailService } from 'src/mail/mail.service'; +import { AdminCreateSubscriptionDTO } from './dto/admin-create-subscription.dto'; +import { BTCPayService } from 'src/payment/providers/services/btcpay.service'; + +/** + * Subscription pricing in USD. + * Since Lightning doesn't support recurring billing, + * each period is a one-time payment that activates the subscription. + */ +const SUBSCRIPTION_PRICES: Record< + string, + { monthly: number; yearly: number } +> = { + enthusiast: { monthly: 9.99, yearly: 99.99 }, + 'film-buff': { monthly: 19.99, yearly: 199.99 }, + cinephile: { monthly: 29.99, yearly: 299.99 }, + 'rss-addon': { monthly: 4.99, yearly: 49.99 }, + 'verification-addon': { monthly: 2.99, yearly: 29.99 }, +}; + +@Injectable() +export class SubscriptionsService { + constructor( + @Inject(UsersService) + private readonly usersService: UsersService, + @InjectRepository(Subscription) + private readonly subscriptionsRepository: Repository, + private readonly _mailService: MailService, + private readonly btcpayService: BTCPayService, + ) {} + + /** + * Get subscription price for a given type and period. + */ + private getPrice(type: SubscriptionType, period: SubscriptionPeriod): number { + const pricing = SUBSCRIPTION_PRICES[type]; + if (!pricing) { + throw new Error(`Unknown subscription type: ${type}`); + } + return period === 'monthly' ? pricing.monthly : pricing.yearly; + } + + /** + * Create a Lightning invoice for a subscription payment. + * Returns the BOLT11 invoice and metadata for the frontend QR display. + */ + async createLightningSubscription( + createSubscriptionDTO: CreateSubscriptionDTO, + user: User, + ) { + const price = this.getPrice( + createSubscriptionDTO.type, + createSubscriptionDTO.period, + ); + + const correlationId = randomUUID(); + const description = `${createSubscriptionDTO.type} subscription (${createSubscriptionDTO.period})`; + + // Create BTCPay invoice + const invoice = await this.btcpayService.issueInvoice( + price, + description, + correlationId, + ); + + // Create a pending subscription record + // We store the BTCPay invoice ID in the stripeId field (legacy column name) + const periodEnd = this.calculatePeriodEnd(createSubscriptionDTO.period); + await this.subscriptionsRepository.save({ + id: correlationId, + stripeId: invoice.invoiceId, + status: 'created', + type: createSubscriptionDTO.type, + period: createSubscriptionDTO.period, + userId: user.id, + periodEnd, + }); + + // Get the BOLT11 invoice + const quote = await this.btcpayService.issueQuote(invoice.invoiceId); + + return { + id: correlationId, + lnInvoice: quote.lnInvoice, + expiration: quote.expiration, + sourceAmount: quote.sourceAmount, + conversionRate: quote.conversionRate, + }; + } + + /** + * Called by webhook when a subscription invoice is paid. + * Activates the subscription. + */ + async activateSubscription(invoiceId: string) { + // stripeId column is repurposed for BTCPay invoice ID + const subscription = await this.subscriptionsRepository.findOne({ + where: { stripeId: invoiceId }, + relations: ['user'], + }); + + if (!subscription) { + Logger.warn( + `No subscription found for BTCPay invoice ${invoiceId}`, + 'SubscriptionsService', + ); + return; + } + + subscription.status = 'succeeded'; + subscription.periodEnd = this.calculatePeriodEnd(subscription.period); + await this.subscriptionsRepository.save(subscription); + + Logger.log( + `Subscription ${subscription.id} activated for user ${subscription.userId}`, + 'SubscriptionsService', + ); + } + + /** + * Calculate the period end date from now. + */ + private calculatePeriodEnd(period: SubscriptionPeriod): Date { + const now = new Date(); + if (period === 'monthly') { + now.setMonth(now.getMonth() + 1); + } else { + now.setFullYear(now.getFullYear() + 1); + } + // Add an extra day of grace + now.setDate(now.getDate() + 1); + return now; + } + + async getActiveSubscriptions(userId: string) { + return this.subscriptionsRepository.find({ + where: { + userId, + status: 'succeeded', + }, + }); + } + + /** + * Admin: manually create a subscription for a user. + */ + async adminCreateSubscription({ + userId, + type, + period, + }: AdminCreateSubscriptionDTO) { + const periodEnd = this.calculatePeriodEnd(period); + return await this.subscriptionsRepository.save({ + id: randomUUID(), + status: 'succeeded', + type, + period, + userId, + periodEnd, + }); + } +} diff --git a/backend/src/subscriptions/tests/flash-subscription.service.spec.ts b/backend/src/subscriptions/tests/flash-subscription.service.spec.ts new file mode 100644 index 0000000..b4b94c7 --- /dev/null +++ b/backend/src/subscriptions/tests/flash-subscription.service.spec.ts @@ -0,0 +1,1279 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ +import { Test, TestingModule } from '@nestjs/testing'; +import { FlashSubscriptionsService } from '../flash-subscription.service'; +import { UsersService } from 'src/users/users.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Subscription } from '../entities/subscription.entity'; +import { Repository } from 'typeorm'; +import { User } from 'src/users/entities/user.entity'; +import { Flash } from '../dto/flash-webhook-event.dto'; +import { Logger } from '@nestjs/common'; +import { SubscriptionsGateway } from 'src/events/subscriptions.gateway'; + +jest.mock('@nestjs/common/services/logger.service'); + +describe('FlashSubscriptionsService', () => { + let service: FlashSubscriptionsService; + let mockUsersService: Partial; + let mockSubscriptionsRepository: Partial>; + const mockSubscriptionsGateway = { + server: { + emit: jest.fn(), + }, + }; + + beforeEach(async () => { + mockUsersService = { + findUserByEmail: jest.fn(), + }; + + mockSubscriptionsRepository = { + create: jest.fn(), + save: jest.fn(), + findOne: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FlashSubscriptionsService, + { provide: UsersService, useValue: mockUsersService }, + { + provide: getRepositoryToken(Subscription), + useValue: mockSubscriptionsRepository, + }, + { + provide: SubscriptionsGateway, + useValue: mockSubscriptionsGateway, + }, + ], + }).compile(); + + service = module.get(FlashSubscriptionsService); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear all mocks after each test + }); + + describe('handleUserSignedUp', () => { + it('should create and save a new subscription for a new user', async () => { + const flashPayload: Flash = { + type: 'enthusiast', + event: { + version: '1.0', + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + (mockSubscriptionsRepository.create as jest.Mock).mockReturnValue({ + ...flashPayload.data, + userId: mockUser.id, + user: mockUser, + status: 'succeeded', + flashEvents: [{ event: flashPayload.event, data: flashPayload.data }], + }); + + await service.handleUserSignedUp(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.create).toHaveBeenCalledWith({ + id: expect.any(String), + stripeId: undefined, + userId: mockUser.id, + user: mockUser, + status: 'succeeded', + type: flashPayload.data.user_plan, + period: 'monthly', + periodEnd: expect.any(Date), + flashEvents: [{ event: flashPayload.event, data: flashPayload.data }], + }); + + expect(mockSubscriptionsRepository.save).toHaveBeenCalled(); + expect(Logger.log).toHaveBeenCalledWith( + 'User "john.doe@example.com" signed up successfully!', + ); + }); + it('should log and skip creating a new subscription if event type invalid', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, // invalid + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [ + { + id: 'sub1', + userId: '1', + user: undefined, + status: 'succeeded', + flashEvents: [flashPayload], + period: 'monthly', + stripeId: undefined, + type: 'enthusiast', + periodEnd: new Date(), + createdAt: new Date(), + }, + ], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + + await service.handleUserSignedUp(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.create).not.toHaveBeenCalled(); + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Event type invalid, expected "1" got "2"', + ); + }); + it('should log and skip creating a new subscription if user already has a sign up event', async () => { + const flashPayload: Flash = { + type: 'enthusiast', + event: { + version: '1.0', + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [ + { + id: 'sub1', + userId: '1', + user: undefined, + status: 'succeeded', + flashEvents: [flashPayload], + period: 'monthly', + stripeId: undefined, + type: 'enthusiast', + periodEnd: new Date(), + createdAt: new Date(), + }, + ], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + + await service.handleUserSignedUp(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.create).not.toHaveBeenCalled(); + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.warn).toHaveBeenCalledWith( + 'already received this account creation event for user john.doe@example.com', + ); + }); + it('should log and skip creating a new subscription if transaction_id is missing', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: '', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + + await service.handleUserSignedUp(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.create).not.toHaveBeenCalled(); + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'no signup payment for user: john.doe@example.com, expected payment', + ); + }); + + it('should log and skip creating a new subscription if failed_payment_date is not empty', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + + await service.handleUserSignedUp(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.create).not.toHaveBeenCalled(); + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Signup payment failed for user: john.doe@example.com, expected success', + ); + }); + }); + describe('handleRenewalSuccessful', () => { + it('should log and skip updating subscription if failed_payment_date is not empty', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '2024-03-15', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + null, + ); + + await service.handleRenewalSuccessful(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'renewal payment failed for user: john.doe@example.com, expected success', + ); + }); + + it('should log and skip updating subscription if event type invalid', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 0, // invalid event type + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '2024-03-15', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + null, + ); + + await service.handleRenewalSuccessful(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Event type invalid, expected "2" got "0"', + ); + }); + + it('should log and skip updating subscription if transaction_id is missing', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '', + transaction_id: '', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue({ + id: 'sub1', + userId: '1', + flashEvents: [], + status: 'active', + periodEnd: new Date(), + user: mockUser, + type: 'enthusiast', + stripeId: undefined, + }); + + await service.handleRenewalSuccessful(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'no renewal payment for user: john.doe@example.com, expected payment', + ); + }); + + it('should log and skip updating subscription if no subscription is found', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + null, + ); + + await service.handleRenewalSuccessful(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Subscription not found for user:', + 'john.doe@example.com', + ); + }); + it('should log and skip updating subscription if duplicate event is detected', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue({ + id: 'sub1', + userId: '1', + flashEvents: [ + { + event: flashPayload.event, + data: { ...flashPayload.data, transaction_id: 'tx123' }, + }, + ], + status: 'active', + periodEnd: new Date(), + user: mockUser, + type: 'enthusiast', + stripeId: undefined, + }); + + await service.handleRenewalSuccessful(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.warn).toHaveBeenCalledWith( + 'Duplicate event detected:', + 'tx123', + ); + }); + + it('should update subscription status, periodEnd, and add new event when all conditions are met', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'rss-addon', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '', + transaction_id: 'tx124', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + const mockSubscription = { + id: 'sub1', + userId: '1', + flashEvents: [], + status: 'active', + periodEnd: new Date(), + user: mockUser, + type: 'rss-addon', + stripeId: undefined, + }; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + mockSubscription, + ); + + await service.handleRenewalSuccessful(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).toHaveBeenCalledWith({ + ...mockSubscription, + flashEvents: [{ event: flashPayload.event, data: flashPayload.data }], + status: 'succeeded', + periodEnd: new Date('2024-04-15'), + type: 'rss-addon', + }); + expect(Logger.log).toHaveBeenCalledWith( + 'User "john.doe@example.com" renewed successfully!', + ); + }); + }); + + describe('handleRenewalFailed', () => { + it('should log and skip updating subscription if no subscription is found', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 3, + name: 'renewal_failed', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: 'tx125', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + null, + ); + + await service.handleRenewalFailed(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Subscription not found for user:', + 'john.doe@example.com', + ); + }); + + it('should log and skip updating subscription if event type invalid', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 7, + name: 'renewal_failed', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: 'tx125', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + null, + ); + + await service.handleRenewalFailed(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Event type invalid, expected "3" got "7"', + ); + }); + + it('should log and skip updating subscription if duplicate failed renewal event is detected', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 3, + name: 'renewal_failed', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: 'tx125', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + const mockSubscription = { + id: 'sub1', + userId: '1', + flashEvents: [ + { + event: flashPayload.event, + data: { ...flashPayload.data, failed_payment_date: '2024-03-15' }, + }, + ], + status: 'active', + periodEnd: new Date(), + user: mockUser, + type: 'enthusiast', + stripeId: undefined, + }; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + mockSubscription, + ); + + await service.handleRenewalFailed(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.warn).toHaveBeenCalledWith( + 'Duplicate failed renewal event detected:', + flashPayload.data, + ); + }); + + it('should log and skip updating subscription if transaction_id is not "-1"', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 3, + name: 'renewal_failed', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: 'tx126', // Not '-1' + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + const mockSubscription = { + id: 'sub1', + userId: '1', + flashEvents: [], + status: 'active', + periodEnd: new Date(), + user: mockUser, + type: 'enthusiast', + stripeId: undefined, + }; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + mockSubscription, + ); + + await service.handleRenewalFailed(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Received a failed event that was not a failed event?:', + flashPayload.data, + ); + }); + + it('should update subscription status, periodEnd, and add new event when all conditions are met', async () => { + const flashPayload: Flash = { + event: { + version: '1.0', + eventType: { + id: 3, + name: 'renewal_failed', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + type: 'rss-addon', + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'rss-addon', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: '-1', // Correct transaction_id for failed event + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + const mockSubscription = { + id: 'sub1', + userId: '1', + flashEvents: [], + status: 'active', + periodEnd: new Date(), + user: mockUser, + type: 'rss-addon', + stripeId: undefined, + }; + + (mockSubscriptionsRepository.findOne as jest.Mock).mockResolvedValue( + mockSubscription, + ); + + await service.handleRenewalFailed(flashPayload, mockUser); + + expect(mockSubscriptionsRepository.save).toHaveBeenCalledWith({ + ...mockSubscription, + flashEvents: [{ event: flashPayload.event, data: flashPayload.data }], + status: 'expired', + periodEnd: new Date('2024-03-15'), + }); + expect(Logger.log).toHaveBeenCalledWith( + 'User "john.doe@example.com" either failed payment, or unsubscribed.', + ); + }); + }); + + describe('updateSubscriptionStatus', () => { + it('should log an error and skip processing if event version is invalid', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '2.0', // Invalid version + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + await service.updateSubscriptionStatus(flashPayload); + + expect(mockUsersService.findUserByEmail).not.toHaveBeenCalled(); + expect(Logger.error).toHaveBeenCalledWith( + 'Can not process Flash Event version "2.0"', + ); + }); + + it('should log an error and skip processing if user is not found', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue(null); + + await service.updateSubscriptionStatus(flashPayload); + + expect(mockUsersService.findUserByEmail).toHaveBeenCalledWith( + 'john.doe@example.com', + ); + expect(Logger.error).toHaveBeenCalledWith( + 'User not found for Flash event: "john.doe@example.com"', + ); + }); + + it('should handle user sign-up event', async () => { + const flashPayload: Flash = { + event: { + version: '1.0', + eventType: { + id: 1, + name: 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + type: 'rss-addon', + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + const handleUserSignedUpSpy = jest + .spyOn(service, 'handleUserSignedUp') + .mockResolvedValueOnce(); + + await service.updateSubscriptionStatus(flashPayload); + + expect(mockUsersService.findUserByEmail).toHaveBeenCalledWith( + 'john.doe@example.com', + ); + expect(Logger.log).toHaveBeenCalledWith('Handling user sign-up'); + expect(handleUserSignedUpSpy).toHaveBeenCalledWith( + flashPayload, + mockUser, + ); + }); + + it('should handle renewal successful event', async () => { + const flashPayload: Flash = { + type: 'rss-addon', + event: { + version: '1.0', + eventType: { + id: 2, + name: 'renewal_successful', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '2024-04-15', + failed_payment_date: '', + transaction_id: 'tx124', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + const handleRenewalSuccessfulSpy = jest + .spyOn(service, 'handleRenewalSuccessful') + .mockResolvedValueOnce(); + + await service.updateSubscriptionStatus(flashPayload); + + expect(mockUsersService.findUserByEmail).toHaveBeenCalledWith( + 'john.doe@example.com', + ); + expect(Logger.log).toHaveBeenCalledWith('Handling successful renewal'); + expect(handleRenewalSuccessfulSpy).toHaveBeenCalledWith( + flashPayload, + mockUser, + ); + }); + it('should handle renewal failed event', async () => { + const flashPayload: Flash = { + type: 'enthusiast', + event: { + version: '1.0', + eventType: { + id: 3, + name: 'renewal_failed', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '2024-03-15', + transaction_id: 'tx125', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + const handleRenewalFailedSpy = jest + .spyOn(service, 'handleRenewalFailed') + .mockResolvedValueOnce(); + + await service.updateSubscriptionStatus(flashPayload); + + expect(mockUsersService.findUserByEmail).toHaveBeenCalledWith( + 'john.doe@example.com', + ); + expect(Logger.log).toHaveBeenCalledWith('Handling failed renewal'); + expect(handleRenewalFailedSpy).toHaveBeenCalledWith( + flashPayload, + mockUser, + ); + }); + + it('should log an error for unknown event type', async () => { + const flashPayload: Flash = { + type: 'enthusiast', + event: { + version: '1.0', + eventType: { + id: 4, // Unknown event type + name: 'unknown_event' as 'user_signed_up', + }, + user_public_key: 'dummy_public_key', + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + }, + data: { + public_key: 'dummy_public_key', + name: 'John Doe', + email: 'john.doe@example.com', + about: 'Test user', + picture_url: 'https://example.com/john.jpg', + user_plan: 'enthusiast', + user_plan_id: '123', + signup_date: '2024-02-15', + next_payment_date: '', + failed_payment_date: '', + transaction_id: 'tx123', + transaction_amount: 1000, + transaction_currency: 'Satoshis', + transaction_date: '', + }, + }; + + const mockUser = { + id: '1', + email: 'john.doe@example.com', + subscriptions: [], + } as User; + + (mockUsersService.findUserByEmail as jest.Mock).mockResolvedValue( + mockUser, + ); + + await service.updateSubscriptionStatus(flashPayload); + + expect(mockUsersService.findUserByEmail).toHaveBeenCalledWith( + 'john.doe@example.com', + ); + expect(Logger.error).toHaveBeenCalledWith( + 'Unknown event type:', + JSON.stringify(flashPayload), + ); + }); + }); +}); diff --git a/backend/src/subscriptions/tests/subscriptions.service.spec.ts b/backend/src/subscriptions/tests/subscriptions.service.spec.ts new file mode 100644 index 0000000..8d522e5 --- /dev/null +++ b/backend/src/subscriptions/tests/subscriptions.service.spec.ts @@ -0,0 +1,514 @@ +/* eslint-disable unicorn/no-null */ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersService } from 'src/users/users.service'; +import { MailService } from 'src/mail/mail.service'; +import { Repository } from 'typeorm'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import Stripe from 'stripe'; +import { Subscription } from '../entities/subscription.entity'; +import { SubscriptionsService } from '../subscriptions.service'; +import { CreateSubscriptionDTO } from '../dto/create-subscription.dto'; +import { + audienceMonthlyRejectedStripe, + audienceMonthlyStripe, + rssAddonMonthlyStripe, + verificationAddonMonthlyStripe, +} from '../../../test/mocks/subscription'; +import { + audienceUserNoSubscription, + audienceUserStripe, + proPlusUserStripe, + ultimateUserStripe, +} from '../../../test/mocks/user'; +import { Logger } from '@nestjs/common'; + +jest.mock('stripe'); // Mock Stripe module +jest.mock('@nestjs/common/services/logger.service'); + +describe('SubscriptionsService', () => { + let service: SubscriptionsService; + let usersService: UsersService; + let mailService: MailService; + let subscriptionsRepository: Repository; + const stripe: jest.Mocked = { + checkout: { sessions: { create: jest.fn() } }, + billingPortal: { + sessions: { create: jest.fn() }, + configurations: { create: jest.fn() }, + }, + subscriptions: { + retrieve: jest.fn(), + }, + } as unknown as jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SubscriptionsService, + { + provide: UsersService, + useValue: { + findUserByEmail: jest.fn(), + }, + }, + { + provide: MailService, + useValue: { sendMail: jest.fn() }, + }, + { + provide: getRepositoryToken(Subscription), + useValue: { + update: jest.fn(), + save: jest.fn(), + findOne: jest.fn(), + findOneOrFail: jest.fn(), + delete: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(SubscriptionsService); + service['stripe'] = stripe; + usersService = module.get(UsersService); + mailService = module.get(MailService); + subscriptionsRepository = module.get>( + getRepositoryToken(Subscription), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createCheckout', () => { + // Mock Stripe's session creation method + it('should create a checkout session', async () => { + const mockDTO: CreateSubscriptionDTO = { + type: 'enthusiast', + period: 'monthly', + }; + const mockSession: any = { url: 'https://stripe.checkout.url' }; + + jest + .spyOn(stripe.checkout.sessions, 'create') + .mockImplementation() + .mockResolvedValue(mockSession); + + jest.spyOn(stripe.subscriptions, 'retrieve').mockResolvedValue({ + items: { + data: [ + { + price: { id: process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID }, + }, + ], + }, + } as any); + + const result = await service.createCheckout( + mockDTO, + audienceUserNoSubscription, + ); + + expect(result).toBe(mockSession.url); + expect(stripe.checkout.sessions.create).toHaveBeenCalledWith({ + mode: 'subscription', + allow_promotion_codes: true, + line_items: [ + { price: process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID, quantity: 1 }, + ], + subscription_data: { trial_period_days: 14 }, + customer_email: audienceUserNoSubscription.email, + payment_method_types: ['card'], + success_url: expect.stringContaining('?refresh=true'), + cancel_url: expect.stringContaining('/subscription'), + }); + }); + }); + + describe('updateSubscription', () => { + it('should update a subscription on webhook event', async () => { + const mockWebhookEvent = { + data: { + object: { id: 'sub_123', object: 'subscription', cancel_at: null }, + }, + }; + + jest + .spyOn(service, 'getByStripeId') + .mockResolvedValue(audienceMonthlyStripe); + jest.spyOn(subscriptionsRepository, 'update').mockResolvedValue({ + affected: 1, + raw: [], + generatedMaps: [], + }); + + await service.updateSubscription(mockWebhookEvent as any); + + expect(subscriptionsRepository.update).toHaveBeenCalledWith( + { id: audienceMonthlyStripe.id }, + { + type: expect.any(String), + period: expect.any(String), + status: 'succeeded', + }, + ); + }); + }); + + describe('notifySubscriptor', () => { + it('should send an email notification to the subscriber', async () => { + jest + .spyOn(service, 'getByStripeId') + .mockResolvedValue(audienceMonthlyStripe); + + jest.spyOn(mailService, 'sendMail').mockResolvedValue(); + + await service.notifySubscriptor( + audienceMonthlyStripe.stripeId, + 'd-62fb0e9d71304edfaed4f4f1781dc6f9', + { + trial_end: Math.floor(Date.now() / 1000) + 14 * 24 * 60 * 60, + }, + ); + + expect(mailService.sendMail).toHaveBeenCalledWith({ + to: audienceMonthlyStripe.user.email, + templateId: 'd-62fb0e9d71304edfaed4f4f1781dc6f9', + data: { + trial_end: expect.any(Number), + }, + }); + }); + }); + + describe('getByStripeId', () => { + it('should return a subscription by Stripe ID', async () => { + jest + .spyOn(subscriptionsRepository, 'findOneOrFail') + .mockResolvedValue(audienceMonthlyStripe); + + const result = await service.getByStripeId(audienceMonthlyStripe.id); + + expect(result).toBe(audienceMonthlyStripe); + expect(subscriptionsRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { stripeId: audienceMonthlyStripe.id }, + relations: ['user'], + }); + }); + }); + + describe('getPriceId', () => { + it('should return the correct price ID', () => { + const type = 'enthusiast'; + const period = 'monthly'; + const result = service['getPriceId'](type, period); + + expect(result).toBe(process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID); + }); + }); + + describe('getTypePeriodFromPriceId', () => { + it('should return the type and period from the price ID', () => { + const mockPriceId = process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID; + const result = service['getTypePeriodFromPriceId'](mockPriceId); + + expect(result).toStrictEqual({ + type: 'enthusiast', + period: 'monthly', + }); + }); + }); + + describe('updateOnCheckoutSessionCompleted', () => { + it('should update subscription on checkout session completed', async () => { + const mockWebhookEvent: any = { + data: { + object: { + subscription: rssAddonMonthlyStripe.stripeId, + payment_status: 'paid', + }, + }, + }; + + const mockStripeSubscription = { + subscription: { + id: rssAddonMonthlyStripe.stripeId, + items: { + data: [ + { + price: { id: process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID }, + }, + ], + }, + customer: { email: proPlusUserStripe.email }, + current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, + }, + }; + + jest + .spyOn(service, 'getStripeSubscription') + .mockResolvedValue(mockStripeSubscription as any); + jest + .spyOn(usersService, 'findUserByEmail') + .mockResolvedValue(proPlusUserStripe); + jest.spyOn(subscriptionsRepository, 'findOne').mockResolvedValue(null); + jest + .spyOn(subscriptionsRepository, 'save') + .mockResolvedValue(rssAddonMonthlyStripe); + + await service.updateOnCheckoutSessionCompleted(mockWebhookEvent); + + expect(service.getStripeSubscription).toHaveBeenCalledWith('sub_123'); + expect(usersService.findUserByEmail).toHaveBeenCalledWith( + proPlusUserStripe.email, + ); + expect(subscriptionsRepository.save).toHaveBeenCalled(); + }); + + it('should notify subscriber if subscription type is audience', async () => { + const mockWebhookEvent: any = { + data: { + object: { + subscription: audienceMonthlyStripe.stripeId, + payment_status: 'paid', + }, + }, + }; + + const mockStripeSubscription = { + subscription: { + id: audienceMonthlyStripe.stripeId, + items: { + data: [ + { + price: { id: process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID }, + }, + ], + }, + customer: { email: audienceUserStripe.email }, + current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, + }, + }; + + jest + .spyOn(service, 'getStripeSubscription') + .mockResolvedValue(mockStripeSubscription as any); + jest + .spyOn(usersService, 'findUserByEmail') + .mockResolvedValue(audienceUserStripe); + jest + .spyOn(subscriptionsRepository, 'findOne') + .mockResolvedValue(audienceMonthlyStripe); + jest.spyOn(service, 'notifySubscriptor').mockResolvedValue(); + + await service.updateOnCheckoutSessionCompleted(mockWebhookEvent); + + expect(service.getStripeSubscription).toHaveBeenCalledWith( + audienceMonthlyStripe.stripeId, + ); + expect(usersService.findUserByEmail).toHaveBeenCalledWith( + audienceUserStripe.email, + ); + expect(service.notifySubscriptor).toHaveBeenCalledWith( + audienceMonthlyStripe.stripeId, + 'd-62fb0e9d71304edfaed4f4f1781dc6f9', + ); + }); + }); + + describe('updateOnInvoicePaymentFailed', () => { + it('should update subscription status to rejected on invoice payment failed', async () => { + const mockWebhookEvent: any = { + data: { + object: { + subscription: audienceMonthlyRejectedStripe.stripeId, + }, + }, + }; + + const mockStripeSubscription = { + subscription: { + id: audienceMonthlyRejectedStripe.stripeId, + current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, + }, + }; + + jest + .spyOn(service, 'getStripeSubscription') + .mockResolvedValue(mockStripeSubscription as any); + jest + .spyOn(subscriptionsRepository, 'findOneOrFail') + .mockResolvedValue(audienceMonthlyRejectedStripe); + jest + .spyOn(subscriptionsRepository, 'save') + .mockResolvedValue(audienceMonthlyRejectedStripe); + + await service.updateOnInvoicePaymentFailed(mockWebhookEvent); + + expect(subscriptionsRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { stripeId: audienceMonthlyRejectedStripe.stripeId }, + }); + expect(subscriptionsRepository.save).toHaveBeenCalledWith({ + ...audienceMonthlyRejectedStripe, + status: 'rejected', + periodEnd: new Date( + mockStripeSubscription.subscription.current_period_end * 1000, + ), + }); + }); + + it('should log an error if subscription is not found on invoice payment failed', async () => { + const mockWebhookEvent: any = { + data: { + object: { + subscription: 'non_existent_subscription_id', + }, + }, + }; + + jest + .spyOn(subscriptionsRepository, 'findOneOrFail') + .mockRejectedValue(new Error('No subscription found')); + + await service.updateOnInvoicePaymentFailed(mockWebhookEvent); + + expect(subscriptionsRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { stripeId: 'non_existent_subscription_id' }, + }); + expect(Logger.error).toHaveBeenCalledTimes(2); + }); + }); + + describe('createBilling', () => { + it('should create a billing portal session', async () => { + const mockStripeSubscription = { + subscription: { + id: rssAddonMonthlyStripe.stripeId, + customer: { id: 'cus_123', email: proPlusUserStripe.email }, + }, + }; + const mockSession = { url: 'https://billing.portal.url' }; + + jest + .spyOn(subscriptionsRepository, 'findOneOrFail') + .mockResolvedValue(rssAddonMonthlyStripe); + jest + .spyOn(service, 'getStripeSubscription') + .mockResolvedValue(mockStripeSubscription as any); + jest + .spyOn(stripe.billingPortal.configurations, 'create') + .mockResolvedValue({ + id: 'config_123', + } as any); + jest + .spyOn(stripe.billingPortal.sessions, 'create') + .mockImplementation() + .mockResolvedValue(mockSession as any); + + const result = await service.createBilling(proPlusUserStripe, 'pro-plus'); + + expect(result).toBe(mockSession.url); + expect(subscriptionsRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { userId: proPlusUserStripe.id }, + order: { createdAt: 'DESC' }, + }); + + expect(service.getStripeSubscription).toHaveBeenCalledWith( + rssAddonMonthlyStripe.stripeId, + ); + expect( + service['stripe'].billingPortal.sessions.create, + ).toHaveBeenCalledWith({ + customer: 'cus_123', + return_url: expect.stringContaining('?refresh=true'), + configuration: expect.any(String), + }); + }); + + it('should delete the subscription and throw an error if creating billing portal fails', async () => { + jest + .spyOn(subscriptionsRepository, 'findOneOrFail') + .mockResolvedValue(verificationAddonMonthlyStripe); + jest + .spyOn(service, 'getStripeSubscription') + .mockRejectedValue(new Error('Stripe error')); + jest.spyOn(subscriptionsRepository, 'delete').mockResolvedValue({ + affected: 1, + raw: [], + }); + + await expect( + service.createBilling(ultimateUserStripe, 'ultimate'), + ).rejects.toThrow('Error creating billing portal'); + + expect(subscriptionsRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { userId: ultimateUserStripe.id }, + order: { createdAt: 'DESC' }, + }); + expect(service.getStripeSubscription).toHaveBeenCalledWith( + verificationAddonMonthlyStripe.stripeId, + ); + expect(subscriptionsRepository.delete).toHaveBeenCalledWith( + verificationAddonMonthlyStripe.id, + ); + }); + }); + + describe('updateOnInvoicePaid', () => { + it('should update subscription status to succeeded on invoice paid', async () => { + const mockWebhookEvent: any = { + data: { + object: { + subscription: audienceMonthlyStripe.stripeId, + }, + }, + }; + + const mockStripeSubscription = { + subscription: { + id: audienceMonthlyStripe.stripeId, + current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, + }, + }; + + jest + .spyOn(service, 'getStripeSubscription') + .mockResolvedValue(mockStripeSubscription as any); + jest + .spyOn(subscriptionsRepository, 'update') + .mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }); + + await service.updateOnInvoicePaid(mockWebhookEvent); + + expect(subscriptionsRepository.update).toHaveBeenCalledWith( + { stripeId: audienceMonthlyStripe.stripeId }, + { + status: 'succeeded', + periodEnd: new Date( + mockStripeSubscription.subscription.current_period_end * 1000 + + 24 * 60 * 60 * 1000, + ), + }, + ); + }); + + it('should log an error if subscription is not found on invoice paid', async () => { + const mockWebhookEvent: any = { + data: { + object: { + subscription: 'non_existent_subscription_id', + }, + }, + }; + + jest + .spyOn(service, 'getStripeSubscription') + .mockRejectedValue(new Error('No subscription found')); + + await service.updateOnInvoicePaid(mockWebhookEvent); + + expect(Logger.error).toHaveBeenCalledWith('No subscription found'); + }); + }); +}); diff --git a/backend/src/trailers/entities/trailer.entity.ts b/backend/src/trailers/entities/trailer.entity.ts new file mode 100644 index 0000000..ed91671 --- /dev/null +++ b/backend/src/trailers/entities/trailer.entity.ts @@ -0,0 +1,41 @@ +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { ContentStatus } from 'src/contents/enums/content-status.enum'; +import { Content } from 'src/contents/entities/content.entity'; +import { Project } from 'src/projects/entities/project.entity'; + +@Entity('trailers') +export class Trailer { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + file: string; + + @Column({ + type: 'varchar', + default: 'processing', + }) + status: ContentStatus; + + @Column('json', { nullable: true }) + metadata?: any; + + @CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' }) + updatedAt: Date; + + @OneToMany(() => Content, (content) => content.trailer) + contents: Content[]; + + @OneToMany(() => Project, (project) => project.trailer) + projects: Project[]; +} diff --git a/backend/src/transcoding-server/transcoding-server.module.ts b/backend/src/transcoding-server/transcoding-server.module.ts new file mode 100644 index 0000000..88bd28e --- /dev/null +++ b/backend/src/transcoding-server/transcoding-server.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TranscodingServerService } from './transcoding-server.service'; +import { BullModule } from '@nestjs/bullmq'; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'transcode', + }), + ], + providers: [TranscodingServerService], + exports: [TranscodingServerService], +}) +export class TranscodingServerModule {} diff --git a/backend/src/transcoding-server/transcoding-server.service.ts b/backend/src/transcoding-server/transcoding-server.service.ts new file mode 100644 index 0000000..4a6d57f --- /dev/null +++ b/backend/src/transcoding-server/transcoding-server.service.ts @@ -0,0 +1,163 @@ +import { + ECSClient, + UpdateServiceCommand, + DescribeServicesCommand, +} from '@aws-sdk/client-ecs'; +import { + AutoScalingClient, + UpdateAutoScalingGroupCommand, + DescribeAutoScalingGroupsCommand, +} from '@aws-sdk/client-auto-scaling'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { Queue } from 'bullmq'; +import { Transcode } from 'src/contents/types/transcode'; + +@Injectable() +export class TranscodingServerService { + ecs: ECSClient; + autoScaling: AutoScalingClient; + clusterName = 'indeehub-cluster-transcoding'; + serviceName = 'transcoding-api-service-production'; + autoScalingGroup = 'transcoding-api-ecs-asg-production'; + minTasks = 0; + maxTasks = 1; + scaleOutThreshold = 1; + scaleInThreshold = 0; + + constructor( + @InjectQueue('transcode') private transcodeQueue: Queue, + ) { + this.ecs = new ECSClient({ + region: 'us-east-1', + credentials: { + secretAccessKey: process.env.AWS_SECRET_KEY, + accessKeyId: process.env.AWS_ACCESS_KEY, + }, + }); + this.autoScaling = new AutoScalingClient({ + region: 'us-east-1', + credentials: { + secretAccessKey: process.env.AWS_SECRET_KEY, + accessKeyId: process.env.AWS_ACCESS_KEY, + }, + }); + } + + // Function to get the number of messages in the queue + async getQueueSize(): Promise { + const jobCount = await this.transcodeQueue.getJobCounts('waiting'); + return jobCount['waiting'] || 0; + } + + // Function to check for ongoing transcoding tasks + async getProcessingCount(): Promise { + const jobCount = await this.transcodeQueue.getJobCounts('active'); + return jobCount['active'] || 0; + } + + // Function to describe the ECS service to get the current task count + async describeService(cluster: string, service: string): Promise { + const command = new DescribeServicesCommand({ + cluster, + services: [service], + }); + const response = await this.ecs.send(command); + const desiredCount = response.services?.[0]?.desiredCount || 0; + return desiredCount; + } + + // Function to scale the ECS service by updating the desired task count + async scaleEcsService( + cluster: string, + service: string, + desiredCount: number, + ) { + const command = new UpdateServiceCommand({ + cluster, + service, + desiredCount, + }); + await this.ecs.send(command); + Logger.log(`ECS service scaled to ${desiredCount} tasks`); + } + + // Function to describe the current desired count of the Auto Scaling group + async describeAutoScalingGroup(asgName: string): Promise { + const command = new DescribeAutoScalingGroupsCommand({ + AutoScalingGroupNames: [asgName], + }); + const response = await this.autoScaling.send(command); + return response.AutoScalingGroups?.[0]?.DesiredCapacity || 0; + } + + // Function to update the desired count of the Auto Scaling group + async scaleAutoScalingGroup(asgName: string, desiredCount: number) { + const command = new UpdateAutoScalingGroupCommand({ + AutoScalingGroupName: asgName, + DesiredCapacity: desiredCount, + }); + await this.autoScaling.send(command); + Logger.log(`Auto Scaling group scaled to ${desiredCount} instances`); + } + + // Run the autoscaling logic every hour + @Cron(CronExpression.EVERY_MINUTE) + async autoscaleEcsService() { + if (process.env.ENVIRONMENT !== 'production') return; + try { + const queueSize = await this.getQueueSize(); + const currentTaskCount = await this.describeService( + this.clusterName, + this.serviceName, + ); + const activeTranscodings = await this.getProcessingCount(); + const currentAsgCount = await this.describeAutoScalingGroup( + this.autoScalingGroup, + ); + + Logger.log( + `Queue size: ${queueSize}, Current task count: ${currentTaskCount}, Active transcodings: ${activeTranscodings}, ASG desired count: ${currentAsgCount}`, + ); + + // If there are no active tasks, scale down both ECS and ASG + if ( + activeTranscodings + queueSize <= this.scaleInThreshold && + (currentTaskCount > this.minTasks || currentAsgCount > this.minTasks) + ) { + await this.scaleEcsService( + this.clusterName, + this.serviceName, + this.minTasks, + ); + await this.scaleAutoScalingGroup(this.autoScalingGroup, this.minTasks); // Scale down ASG + Logger.log( + 'Scaled down to 0 tasks and 0 instances in the Auto Scaling group.', + 'TRANSCODING SERVER', + ); + } else if ( + queueSize >= this.scaleOutThreshold && + currentTaskCount < this.maxTasks + ) { + Logger.log( + 'Scaling out ECS and ASG to handle increased load', + 'TRANSCODING SERVER', + ); + // Scale out ECS and ASG + const newTaskCount = Math.min(currentTaskCount + 1, this.maxTasks); + await this.scaleEcsService( + this.clusterName, + this.serviceName, + newTaskCount, + ); + await this.scaleAutoScalingGroup( + this.autoScalingGroup, + Math.min(currentAsgCount + 1, this.maxTasks), + ); + } + } catch (error) { + Logger.error('Error during autoscaling:', error); + } + } +} diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts new file mode 100644 index 0000000..8eb90b5 --- /dev/null +++ b/backend/src/types/express.d.ts @@ -0,0 +1,18 @@ +import type { Event } from 'nostr-tools'; + +declare module 'express-serve-static-core' { + interface Request { + /** + * Unparsed request body captured by express.json verify hook. + */ + rawBody?: string; + /** + * Nostr public key extracted by the NostrAuthGuard. + */ + nostrPubkey?: string; + /** + * Full Nostr event attached for downstream inspection/debugging. + */ + nostrEvent?: Event; + } +} diff --git a/backend/src/types/raw-body-request.ts b/backend/src/types/raw-body-request.ts new file mode 100644 index 0000000..2a09814 --- /dev/null +++ b/backend/src/types/raw-body-request.ts @@ -0,0 +1,9 @@ +import { Request } from 'express'; + +/** + * Express request wrapper that preserves the unparsed body for signature + * verification (e.g., Stripe, Nostr). + */ +export type RawBodyRequest = T & { + rawBody?: string; +}; diff --git a/backend/src/upload/dto/request/finalize-upload.dto.ts b/backend/src/upload/dto/request/finalize-upload.dto.ts new file mode 100644 index 0000000..5f1b01f --- /dev/null +++ b/backend/src/upload/dto/request/finalize-upload.dto.ts @@ -0,0 +1,21 @@ +import { IsArray, IsOptional, IsString } from 'class-validator'; + +export class FinalizeUploadDTO { + @IsString() + @IsOptional() + Bucket: string; + + @IsString() + Key: string; + + @IsString() + UploadId: string; + + @IsArray() + parts: Part[]; +} + +interface Part { + PartNumber: number; + ETag: string; +} diff --git a/backend/src/upload/dto/request/initialize-upload.dto.ts b/backend/src/upload/dto/request/initialize-upload.dto.ts new file mode 100644 index 0000000..4e5707d --- /dev/null +++ b/backend/src/upload/dto/request/initialize-upload.dto.ts @@ -0,0 +1,17 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class InitializeUploadDTO { + @IsString() + @IsOptional() + Bucket: string; + + @IsString() + Key: string; + + @IsString() + @IsOptional() + ContentDisposition: string; + + @IsString() + ContentType: string; +} diff --git a/backend/src/upload/dto/request/presigned-urls.dto.ts b/backend/src/upload/dto/request/presigned-urls.dto.ts new file mode 100644 index 0000000..f0f220d --- /dev/null +++ b/backend/src/upload/dto/request/presigned-urls.dto.ts @@ -0,0 +1,16 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +export class PresignedUrlsDTO { + @IsString() + @IsOptional() + Bucket: string; + + @IsString() + Key: string; + + @IsString() + UploadId: string; + + @IsNumber() + parts: number; +} diff --git a/backend/src/upload/upload.controller.ts b/backend/src/upload/upload.controller.ts new file mode 100644 index 0000000..1253684 --- /dev/null +++ b/backend/src/upload/upload.controller.ts @@ -0,0 +1,26 @@ +import { Body, Controller, HttpCode, Post } from '@nestjs/common'; +import { UploadService } from './upload.service'; +import { InitializeUploadDTO } from './dto/request/initialize-upload.dto'; +import { PresignedUrlsDTO } from './dto/request/presigned-urls.dto'; +import { FinalizeUploadDTO } from './dto/request/finalize-upload.dto'; + +@Controller('upload') +export class UploadController { + constructor(private readonly uploadService: UploadService) {} + + @Post('initialize') + async initialize(@Body() initializeUploadDTO: InitializeUploadDTO) { + return this.uploadService.initialize(initializeUploadDTO); + } + + @Post('presigned-urls') + async presignedUrls(@Body() presignedUrlsDTO: PresignedUrlsDTO) { + return this.uploadService.presignedUrls(presignedUrlsDTO); + } + + @Post('finalize') + @HttpCode(200) + async finalize(@Body() finalizeUploadDTO: FinalizeUploadDTO) { + return this.uploadService.finalize(finalizeUploadDTO); + } +} diff --git a/backend/src/upload/upload.module.ts b/backend/src/upload/upload.module.ts new file mode 100644 index 0000000..28887f2 --- /dev/null +++ b/backend/src/upload/upload.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { UploadController } from './upload.controller'; +import { UploadService } from './upload.service'; + +@Global() +@Module({ + controllers: [UploadController], + providers: [UploadService], + exports: [UploadService], +}) +export class UploadModule {} diff --git a/backend/src/upload/upload.service.ts b/backend/src/upload/upload.service.ts new file mode 100644 index 0000000..625a4d0 --- /dev/null +++ b/backend/src/upload/upload.service.ts @@ -0,0 +1,234 @@ +import { + CompleteMultipartUploadCommand, + CopyObjectCommand, + CreateMultipartUploadCommand, + DeleteObjectsCommand, + HeadObjectCommand, + ListObjectsCommand, + S3Client, + UploadPartCommand, +} from '@aws-sdk/client-s3'; + +import { Injectable, Logger } from '@nestjs/common'; +import { InitializeUploadDTO } from './dto/request/initialize-upload.dto'; +import { PresignedUrlsDTO } from './dto/request/presigned-urls.dto'; +import { FinalizeUploadDTO } from './dto/request/finalize-upload.dto'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { getSignedUrl as getCloudfrontSignedUrl } from '@aws-sdk/cloudfront-signer'; +import { getPrivateS3Url } from 'src/common/helper'; + +@Injectable() +export class UploadService { + private s3: S3Client; + constructor() { + const s3Config: any = { + region: process.env.AWS_REGION || 'us-east-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY, + secretAccessKey: process.env.AWS_SECRET_KEY, + }, + }; + + // MinIO compatibility: if S3_ENDPOINT is set, override the endpoint + // and force path-style access (MinIO requires this) + if (process.env.S3_ENDPOINT) { + s3Config.endpoint = process.env.S3_ENDPOINT; + s3Config.forcePathStyle = true; + } + + this.s3 = new S3Client(s3Config); + } + + async initialize({ + Bucket, + Key, + ContentDisposition, + ContentType, + }: InitializeUploadDTO) { + const multipartParameters = { + Bucket: Bucket ?? process.env.S3_PRIVATE_BUCKET_NAME, + Key, + ContentDisposition: ContentDisposition ?? 'inline', + ContentType, + }; + const command = new CreateMultipartUploadCommand(multipartParameters); + const multipartUpload = await this.s3.send(command); + return multipartUpload; + } + + async presignedUrls({ Bucket, Key, UploadId, parts }: PresignedUrlsDTO) { + const multipartParameters = { + Bucket: Bucket ?? process.env.S3_PRIVATE_BUCKET_NAME, + Key, + UploadId, + }; + + const promises = []; + + for (let index = 0; index < parts; index++) { + const command = new UploadPartCommand({ + ...multipartParameters, + PartNumber: index + 1, + }); + promises.push( + getSignedUrl(this.s3, command, { + expiresIn: 60 * 60 * 24 * 7, + }), + ); + } + + const signedUrls = await Promise.all(promises); + + const mappedUrls = signedUrls.map((signedUrl, index) => { + return { + signedUrl, + PartNumber: index + 1, + }; + }); + + return { parts: mappedUrls }; + } + + async finalize({ Bucket, Key, UploadId, parts }: FinalizeUploadDTO) { + parts.sort((a, b) => { + return a.PartNumber - b.PartNumber; + }); + + const multipartParameters = { + Bucket: Bucket ?? process.env.S3_PRIVATE_BUCKET_NAME, + Key, + UploadId, + MultipartUpload: { + Parts: parts, + }, + }; + + const command = new CompleteMultipartUploadCommand(multipartParameters); + await this.s3.send(command); + + return { + statusCode: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + }; + } + + async pruneFolder(url: string, bucket: string) { + try { + // remove last / to get folder + const folderUrl = url.slice(0, Math.max(0, url.lastIndexOf('/'))); + const parameters = { + Bucket: bucket, + Prefix: folderUrl, + }; + const command = new ListObjectsCommand(parameters); + const { Contents } = await this.s3.send(command); + if (!Contents) return; + const objects = Contents.filter((content) => content.Key !== url).map( + (content) => ({ Key: content.Key }), + ); + + const deleteParameters = { + Bucket: bucket, + Delete: { + Objects: objects, + Quiet: false, + }, + }; + const deleteCommand = new DeleteObjectsCommand(deleteParameters); + await this.s3.send(deleteCommand); + } catch (error) { + Logger.error('Error pruning folder: ' + error); + } + } + + async deleteObject(url: string, bucket: string) { + try { + const command = new DeleteObjectsCommand({ + Bucket: bucket, + Delete: { + Objects: [ + { + Key: url, + }, + ], + Quiet: false, + }, + }); + await this.s3.send(command); + } catch (error) { + Logger.error('Error deleting object: ' + error + ' - ' + url); + } + } + + async createPresignedUrl({ + key, + expires, + }: { + key: string; + expires?: number; + }) { + const privateKey = process.env.CLOUDFRONT_PRIVATE_KEY; + const keyPairId = process.env.CLOUDFRONT_KEY_PAIR_ID; + + // If CloudFront is not configured (MinIO/self-hosted mode), + // generate an S3 presigned URL instead + if (!privateKey || !keyPairId) { + const command = new HeadObjectCommand({ + Bucket: process.env.S3_PRIVATE_BUCKET_NAME, + Key: key, + }); + return getSignedUrl(this.s3, command, { + expiresIn: expires ?? 60 * 60 * 24 * 7, + }); + } + + const dateLessThan = Date.now() + (expires ?? 60 * 60 * 24 * 7) * 1000; + + const url = getCloudfrontSignedUrl({ + url: getPrivateS3Url(key), + dateLessThan: new Date(dateLessThan).toISOString(), + keyPairId, + privateKey: privateKey.replaceAll(String.raw`\n`, '\n'), + }); + + return url; + } + + async objectExists(key: string, bucket: string): Promise { + try { + const command = new HeadObjectCommand({ + Bucket: bucket, + Key: key, + }); + await this.s3.send(command); + return true; + } catch (error) { + const statusCode = error?.$metadata?.httpStatusCode; + if (error?.name === 'NotFound' || statusCode === 404) { + return false; + } + Logger.error(`Error checking object existence: ${error}`); + return false; + } + } + + async copyObject({ + sourceBucket, + destinationBucket, + key, + }: { + sourceBucket: string; + destinationBucket: string; + key: string; + }) { + const encodedKey = encodeURIComponent(key); + const command = new CopyObjectCommand({ + Bucket: destinationBucket, + Key: key, + CopySource: `${sourceBucket}/${encodedKey}`, + }); + await this.s3.send(command); + } +} diff --git a/backend/src/users/dto/request/create-user.dto.ts b/backend/src/users/dto/request/create-user.dto.ts new file mode 100644 index 0000000..94e9a9f --- /dev/null +++ b/backend/src/users/dto/request/create-user.dto.ts @@ -0,0 +1,13 @@ +import { IsEmail, IsString } from 'class-validator'; + +export class CreateUserDTO { + @IsString() + legalName: string; + + @IsString() + @IsEmail() + email: string; + + @IsString() + cognitoId: string; +} diff --git a/backend/src/users/dto/request/update-user.dto.ts b/backend/src/users/dto/request/update-user.dto.ts new file mode 100644 index 0000000..5a5cac5 --- /dev/null +++ b/backend/src/users/dto/request/update-user.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateUserDTO } from './create-user.dto'; +import { UpdateFilmmakerDTO } from 'src/filmmakers/dto/request/update-filmmaker.dto'; +import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class UpdateUserDTO extends PartialType(CreateUserDTO) { + @IsString() + @IsOptional() + profilePictureUrl?: string; + + @ValidateNested() + @ApiProperty({ type: () => UpdateFilmmakerDTO }) + @Type(() => UpdateFilmmakerDTO) + filmmaker?: UpdateFilmmakerDTO; +} diff --git a/backend/src/users/dto/response/user.dto.ts b/backend/src/users/dto/response/user.dto.ts new file mode 100644 index 0000000..3ca62dc --- /dev/null +++ b/backend/src/users/dto/response/user.dto.ts @@ -0,0 +1,42 @@ +import { FilmmakerDTO } from 'src/filmmakers/dto/response/filmmaker.dto'; +import { SubscriptionDTO } from 'src/subscriptions/dto/response/subscription.dto'; +import { User } from 'src/users/entities/user.entity'; + +export class UserDTO { + id: string; + email: string; + cognitoId: string; + filmmaker?: FilmmakerDTO; + subscriptions?: SubscriptionDTO[]; + legalName: string; + lastName: string; + profilePictureUrl: string; + + constructor(user: User) { + this.id = user.id; + this.email = user.email; + this.cognitoId = user.cognitoId; + if (user.filmmaker) { + this.filmmaker = new FilmmakerDTO(user.filmmaker); + } + if (user.subscriptions && user.subscriptions.length > 0) { + user.subscriptions.sort( + (a, b) => b.periodEnd.getTime() - a.periodEnd.getTime(), + ); + const activeSubscriptions = user.subscriptions.filter( + (subscription) => + subscription.periodEnd > new Date() && + (subscription.status === 'succeeded' || + subscription.status === 'cancelled'), + ); + this.subscriptions = activeSubscriptions.map( + (activeSubscription) => new SubscriptionDTO(activeSubscription), + ); + } + this.legalName = user.legalName; + if (user.profilePictureUrl) { + this.profilePictureUrl = + process.env.S3_PUBLIC_BUCKET_URL + user.profilePictureUrl; + } + } +} diff --git a/backend/src/users/entities/user.entity.ts b/backend/src/users/entities/user.entity.ts new file mode 100644 index 0000000..f459c13 --- /dev/null +++ b/backend/src/users/entities/user.entity.ts @@ -0,0 +1,68 @@ +import { DiscountRedemption } from 'src/discount-redemption/entities/discount-redemption.entity'; +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { LibraryItem } from 'src/library/entities/library-item.entity'; +import { Payout } from 'src/payout/entities/payout.entity'; +import { Rent } from 'src/rents/entities/rent.entity'; +import { SeasonRent } from 'src/season/entities/season-rents.entity'; +import { Subscription } from 'src/subscriptions/entities/subscription.entity'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + OneToMany, + OneToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('users') +export class User { + @PrimaryColumn() + id: string; + + @Column() + email: string; + + @Column({ unique: true, nullable: true }) + cognitoId: string; + + @Column({ unique: true, nullable: true }) + nostrPubkey?: string; + + @Column() + legalName: string; + + @Column({ nullable: true }) + profilePictureUrl: string; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date; + + @OneToOne(() => Filmmaker, (filmmaker) => filmmaker.user) + filmmaker?: Filmmaker; + + @OneToMany(() => Subscription, (sub) => sub.user) + subscriptions: Subscription[]; + + @OneToMany(() => Rent, (rent) => rent.user) + rents?: Rent[]; + + @OneToMany(() => SeasonRent, (seasonRent) => seasonRent.user) + seasonRents: SeasonRent[]; + + @OneToMany(() => Payout, (payout) => payout.user) + payouts?: Payout[]; + + @OneToMany(() => DiscountRedemption, (redemption) => redemption.user) + redemptions: DiscountRedemption[]; + + @OneToMany(() => LibraryItem, (item) => item.user) + libraryItems: LibraryItem[]; +} diff --git a/backend/src/users/guards/user.guard.ts b/backend/src/users/guards/user.guard.ts new file mode 100644 index 0000000..276c4b9 --- /dev/null +++ b/backend/src/users/guards/user.guard.ts @@ -0,0 +1,18 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class UserGuard implements CanActivate { + constructor() {} + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + + const userId = request.params.id; + + const user = request.user; + + return userId === user.id; + } +} diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts new file mode 100644 index 0000000..6bac27a --- /dev/null +++ b/backend/src/users/users.controller.ts @@ -0,0 +1,31 @@ +import { + Controller, + Body, + Patch, + Param, + UseGuards, + Delete, +} from '@nestjs/common'; +import { UsersService } from './users.service'; +import { UpdateUserDTO } from './dto/request/update-user.dto'; +import { UserGuard } from './guards/user.guard'; +import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { UserDTO } from './dto/response/user.dto'; +import { AdminAuthGuard } from 'src/auth/guards/admin.guard'; + +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Patch(':id') + @UseGuards(HybridAuthGuard, UserGuard) + async update(@Param('id') id: string, @Body() updateUserDTO: UpdateUserDTO) { + return new UserDTO(await this.usersService.update(id, updateUserDTO)); + } + + @Delete(':id') + @UseGuards(AdminAuthGuard) + async delete(@Param('id') id: string) { + return await this.usersService.delete(id); + } +} diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts new file mode 100644 index 0000000..d77cdc1 --- /dev/null +++ b/backend/src/users/users.module.ts @@ -0,0 +1,26 @@ +import { Global, Module, forwardRef } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from './entities/user.entity'; +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; +import { UsersController } from './users.controller'; +import { FilmmakersModule } from 'src/filmmakers/filmmakers.module'; +import { UploadModule } from 'src/upload/upload.module'; +import { MailModule } from 'src/mail/mail.module'; +import { AuthModule } from 'src/auth/auth.module'; +import { DiscountRedemption } from 'src/discount-redemption/entities/discount-redemption.entity'; + +@Global() +@Module({ + imports: [ + TypeOrmModule.forFeature([User, Filmmaker, DiscountRedemption]), + forwardRef(() => FilmmakersModule), + UploadModule, + MailModule, + forwardRef(() => AuthModule), + ], + providers: [UsersService], + exports: [UsersService], + controllers: [UsersController], +}) +export class UsersModule {} diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts new file mode 100644 index 0000000..2611408 --- /dev/null +++ b/backend/src/users/users.service.ts @@ -0,0 +1,202 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateUserDTO } from './dto/request/create-user.dto'; +import { User } from './entities/user.entity'; +import { randomUUID } from 'node:crypto'; +import { UpdateUserDTO } from './dto/request/update-user.dto'; +import { FilmmakersService } from 'src/filmmakers/filmmakers.service'; +import { UploadService } from 'src/upload/upload.service'; +import { MailService } from 'src/mail/mail.service'; +import { AuthService } from 'src/auth/auth.service'; + +export interface ICreateUser { + userDto: CreateUserDTO; + templateId?: string; + data?: Record; + userType?: 'audience' | 'filmmaker'; +} +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + @Inject(FilmmakersService) + private readonly filmmakersService: FilmmakersService, + @Inject(UploadService) + private readonly uploadService: UploadService, + @Inject(MailService) + private readonly mailService: MailService, + @Inject(AuthService) + private readonly authService: AuthService, + ) {} + + async createUser({ + userDto: dto, + templateId, + data, + userType, + }: ICreateUser): Promise { + const user = this.userRepository.create({ + id: randomUUID(), + legalName: dto.legalName, + email: dto.email, + cognitoId: dto.cognitoId, + }); + + const defaultTemplateId = + userType === 'filmmaker' + ? 'd-39ebc41a80de4318b4e59ca0d398d70d' + : 'd-ef5b24f8c80d454b8d6d230f41832cfe'; + + this.mailService.sendMail({ + templateId: templateId ?? defaultTemplateId, + to: user.email, + data, + }); + return await this.userRepository.save(user); + } + + async findUsersById(id: string): Promise { + const user = await this.userRepository.findOne({ + where: { id }, + relations: ['filmmaker', 'subscriptions'], + relationLoadStrategy: 'query', + }); + return user; + } + + async findUserByEmail(email: string): Promise { + const user = await this.userRepository.findOne({ + where: { email }, + relations: ['filmmaker', 'subscriptions'], + // load only the most recent subscription + relationLoadStrategy: 'query', + }); + return user; + } + + async findUserByCognitoId(cognitoId: string): Promise { + const user = await this.userRepository.findOne({ + where: { cognitoId }, + relations: ['filmmaker', 'subscriptions'], + // load only the most recent subscription + relationLoadStrategy: 'query', + }); + return user; + } + + async findUserByNostrPubkey(nostrPubkey: string): Promise { + return await this.userRepository.findOne({ + where: { nostrPubkey }, + relations: ['filmmaker', 'subscriptions'], + relationLoadStrategy: 'query', + }); + } + + /** + * Auto-create a user + filmmaker record for a Nostr-only login. + * Every Nostr user is treated as a potential creator (filmmaker). + * Uses a generated placeholder email so the NOT-NULL constraint is satisfied. + */ + async createNostrUser(nostrPubkey: string): Promise { + const shortKey = nostrPubkey.slice(0, 12); + const userId = randomUUID(); + + const user = this.userRepository.create({ + id: userId, + nostrPubkey, + email: `${shortKey}@nostr.local`, + legalName: `Nostr ${shortKey}`, + }); + const savedUser = await this.userRepository.save(user); + + // Auto-create a filmmaker profile so the user can access Backstage + try { + await this.filmmakersService.create({ + userId, + professionalName: `Nostr ${shortKey}`, + }); + } catch (error) { + Logger.warn( + `Could not auto-create filmmaker for ${shortKey}: ${error.message}`, + 'UsersService', + ); + } + + // Re-fetch with relations so the filmmaker is included + return ( + (await this.findUserByNostrPubkey(nostrPubkey)) ?? savedUser + ); + } + + /** + * Ensure a user has a filmmaker profile — creates one if missing. + * Returns the refreshed user with the filmmaker relation loaded. + */ + async ensureFilmmaker(user: User): Promise { + if (user.filmmaker) return user; + + const shortKey = + user.nostrPubkey?.slice(0, 12) ?? user.email.split('@')[0]; + await this.filmmakersService.create({ + userId: user.id, + professionalName: `Nostr ${shortKey}`, + }); + + return (await this.findUserByNostrPubkey(user.nostrPubkey)) ?? user; + } + + async linkNostrPubkey(userId: string, nostrPubkey: string): Promise { + const existingUserWithKey = await this.userRepository.findOneBy({ + nostrPubkey, + }); + + if (existingUserWithKey && existingUserWithKey.id !== userId) { + throw new Error('Nostr pubkey already linked to another user'); + } + + const user = await this.findUsersById(userId); + user.nostrPubkey = nostrPubkey; + return await this.userRepository.save(user); + } + + async unlinkNostrPubkey(userId: string): Promise { + const user = await this.findUsersById(userId); + user.nostrPubkey = undefined; + return await this.userRepository.save(user); + } + + async update(id: string, dto: UpdateUserDTO): Promise { + const user = await this.findUsersById(id); + user.legalName = dto.legalName; + user.email = dto.email; + user.profilePictureUrl = dto.profilePictureUrl; + if (dto.profilePictureUrl) { + this.uploadService.pruneFolder( + user.profilePictureUrl, + process.env.S3_PUBLIC_BUCKET_NAME, + ); + } + if (dto.filmmaker && user.filmmaker) { + await this.filmmakersService.update(user.filmmaker.id, dto.filmmaker); + } + return await this.userRepository.save(user); + } + + async delete(id: string) { + const user = await this.findUsersById(id); + Logger.log(`Deleting user ${user.email}`); + if (user.filmmaker) { + await this.filmmakersService.delete(user.filmmaker.id); + } + await this.authService.deleteUserFromCognito(user.email); + if (user.profilePictureUrl) { + this.uploadService.deleteObject( + user.profilePictureUrl, + process.env.S3_PUBLIC_BUCKET_NAME, + ); + } + return await this.userRepository.softDelete({ id }); + } +} diff --git a/backend/src/waitlist/waitlist.controller.ts b/backend/src/waitlist/waitlist.controller.ts new file mode 100644 index 0000000..7f1f45c --- /dev/null +++ b/backend/src/waitlist/waitlist.controller.ts @@ -0,0 +1,11 @@ +import { Body, Controller, Put } from '@nestjs/common'; +import { WaitlistService } from './waitlist.service'; + +@Controller('waitlist') +export class WaitlistController { + constructor(private readonly waitlistService: WaitlistService) {} + @Put() + async addToNewsletter(@Body('email') email: string) { + return await this.waitlistService.addToNewsletter(email); + } +} diff --git a/backend/src/waitlist/waitlist.module.ts b/backend/src/waitlist/waitlist.module.ts new file mode 100644 index 0000000..68f3000 --- /dev/null +++ b/backend/src/waitlist/waitlist.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { WaitlistController } from './waitlist.controller'; +import { WaitlistService } from './waitlist.service'; + +@Module({ + controllers: [WaitlistController], + providers: [WaitlistService], +}) +export class WaitlistModule {} diff --git a/backend/src/waitlist/waitlist.service.ts b/backend/src/waitlist/waitlist.service.ts new file mode 100644 index 0000000..5a63d90 --- /dev/null +++ b/backend/src/waitlist/waitlist.service.ts @@ -0,0 +1,23 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { MailService } from 'src/mail/mail.service'; + +@Injectable() +export class WaitlistService { + constructor(private readonly mailService: MailService) {} + + async addToNewsletter(email: string): Promise { + try { + await this.mailService.addToList( + [email], + [process.env.SENDGRID_WAITLIST], + ); + await this.mailService.sendMail({ + templateId: 'd-39ebc41a80de4318b4e59ca0d398d70d', + to: email, + }); + return true; + } catch (error) { + throw new BadRequestException(error.response.body.errors[0].message); + } + } +} diff --git a/backend/src/webhooks/dto/strike-webhook.dto.ts b/backend/src/webhooks/dto/strike-webhook.dto.ts new file mode 100644 index 0000000..ce21786 --- /dev/null +++ b/backend/src/webhooks/dto/strike-webhook.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsObject, IsString } from 'class-validator'; + +export class StrikeWebhookData { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + eventType: string; + + @ApiProperty() + @IsObject() + data: { + entityId: string; + changes: string[]; + }; +} diff --git a/backend/src/webhooks/guards/btcpay.guard.ts b/backend/src/webhooks/guards/btcpay.guard.ts new file mode 100644 index 0000000..82d4daf --- /dev/null +++ b/backend/src/webhooks/guards/btcpay.guard.ts @@ -0,0 +1,55 @@ +import { Injectable, CanActivate, ExecutionContext, Logger } from '@nestjs/common'; +import { createHmac } from 'node:crypto'; + +/** + * Guard for BTCPay Server webhook requests. + * + * BTCPay signs webhooks with HMAC-SHA256. The signature is sent + * in the `BTCPay-Sig` header in the format `sha256=HEXDIGEST`. + * We verify against BTCPAY_WEBHOOK_SECRET. + */ +@Injectable() +export class BTCPayGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + if (process.env.ENVIRONMENT === 'development' || process.env.ENVIRONMENT === 'local') { + return true; + } + + const request = context.switchToHttp().getRequest(); + const signatureHeader = request.headers['btcpay-sig']; + + if (!signatureHeader) { + Logger.warn('BTCPay webhook missing BTCPay-Sig header', 'BTCPayGuard'); + return false; + } + + const rawBody = request.rawBody; + if (!rawBody) { + Logger.warn('BTCPay webhook missing raw body', 'BTCPayGuard'); + return false; + } + + return this.validateSignature(signatureHeader, rawBody); + } + + private validateSignature(signatureHeader: string, rawBody: Buffer | string): boolean { + const secret = process.env.BTCPAY_WEBHOOK_SECRET || process.env.BTCPAY_WEBHOOK_SECRET; + if (!secret) { + Logger.error('BTCPAY_WEBHOOK_SECRET not configured', 'BTCPayGuard'); + return false; + } + + // BTCPay sends: "sha256=HEXDIGEST" + const expectedPrefix = 'sha256='; + if (!signatureHeader.startsWith(expectedPrefix)) { + return false; + } + + const receivedHash = signatureHeader.slice(expectedPrefix.length); + const computedHash = createHmac('sha256', secret) + .update(rawBody) + .digest('hex'); + + return computedHash.toLowerCase() === receivedHash.toLowerCase(); + } +} diff --git a/backend/src/webhooks/guards/strike.guard.ts b/backend/src/webhooks/guards/strike.guard.ts new file mode 100644 index 0000000..6bece74 --- /dev/null +++ b/backend/src/webhooks/guards/strike.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { createHmac } from 'node:crypto'; + +@Injectable() +export class StrikeGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean | Promise { + if (process.env.ENVIROMENT === 'development') return true; + const request = context.switchToHttp().getRequest(); + const signatureHeader = request.headers['x-webhook-signature']; + if (!signatureHeader || !request.rawBody) return false; + return validateSignature(signatureHeader, request.rawBody); + } +} + +const validateSignature = (signatureHeader: string, rawBody: string) => { + const computedSignature = createHmac('sha256', process.env.STRIKE_WEBHOOK_KEY) + .update(rawBody) + .digest('hex'); + return computedSignature.toUpperCase() === signatureHeader.toUpperCase(); +}; diff --git a/backend/src/webhooks/webhook.module.ts b/backend/src/webhooks/webhook.module.ts new file mode 100644 index 0000000..d2bf9ed --- /dev/null +++ b/backend/src/webhooks/webhook.module.ts @@ -0,0 +1,13 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { WebhooksController } from './webhooks.controller'; +import { WebhooksService } from './webhooks.service'; +import { RentsModule } from 'src/rents/rents.module'; +import { SeasonModule } from 'src/season/season.module'; +import { SubscriptionsModule } from 'src/subscriptions/subscriptions.module'; + +@Module({ + controllers: [WebhooksController], + imports: [SubscriptionsModule, RentsModule, forwardRef(() => SeasonModule)], + providers: [WebhooksService], +}) +export class WebhooksModule {} diff --git a/backend/src/webhooks/webhooks.controller.ts b/backend/src/webhooks/webhooks.controller.ts new file mode 100644 index 0000000..93dc0a2 --- /dev/null +++ b/backend/src/webhooks/webhooks.controller.ts @@ -0,0 +1,25 @@ +import { + Controller, + Post, + Body, + UseGuards, + BadRequestException, +} from '@nestjs/common'; +import { WebhooksService } from './webhooks.service'; +import { BTCPayGuard } from './guards/btcpay.guard'; +import type { BTCPayWebhookEvent } from 'src/payment/providers/dto/btcpay/btcpay-invoice'; + +@Controller('webhooks') +export class WebhooksController { + constructor(private readonly webhooksService: WebhooksService) {} + + @Post('btcpay-webhook') + @UseGuards(BTCPayGuard) + async btcpayPayment(@Body() body: BTCPayWebhookEvent) { + try { + return await this.webhooksService.btcpayWebhook(body); + } catch (error) { + throw new BadRequestException(error.message); + } + } +} diff --git a/backend/src/webhooks/webhooks.service.ts b/backend/src/webhooks/webhooks.service.ts new file mode 100644 index 0000000..392dae3 --- /dev/null +++ b/backend/src/webhooks/webhooks.service.ts @@ -0,0 +1,88 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { RentsService } from 'src/rents/rents.service'; +import { BTCPayService } from 'src/payment/providers/services/btcpay.service'; +import { SeasonRentsService } from 'src/season/season-rents.service'; +import { SubscriptionsService } from 'src/subscriptions/subscriptions.service'; +import type { BTCPayWebhookEvent } from 'src/payment/providers/dto/btcpay/btcpay-invoice'; + +@Injectable() +export class WebhooksService { + constructor( + @Inject(RentsService) + private readonly rentService: RentsService, + @Inject(SeasonRentsService) + private readonly seasonRentService: SeasonRentsService, + @Inject(SubscriptionsService) + private readonly subscriptionsService: SubscriptionsService, + @Inject(BTCPayService) + private readonly btcpayService: BTCPayService, + ) {} + + /** + * Handle BTCPay Server webhook events. + * + * Relevant events: + * - InvoiceSettled: Payment confirmed — mark rent/subscription as paid + * - InvoiceExpired: Invoice expired without payment + * - InvoiceInvalid: Invoice marked invalid + * - InvoicePaymentSettled: Individual payment within invoice settled + */ + async btcpayWebhook(event: BTCPayWebhookEvent): Promise { + Logger.log( + `BTCPay webhook: ${event.type} for invoice ${event.invoiceId}`, + 'WebhooksService', + ); + + switch (event.type) { + case 'InvoiceSettled': + case 'InvoicePaymentSettled': { + try { + const invoiceId = event.invoiceId; + const invoice = await this.btcpayService.getInvoice(invoiceId); + + // Check if this is a content rental + const isContentRental = + await this.rentService.rentByProviderIdExists(invoiceId); + + if (isContentRental) { + return await this.rentService.lightningPaid(invoiceId, invoice); + } + + // Check if it's a subscription payment + try { + await this.subscriptionsService.activateSubscription(invoiceId); + return; + } catch { + // Not a subscription — continue to season check + } + + // Otherwise check if it's a season rental + return await this.seasonRentService.lightningPaid(invoiceId, invoice); + } catch (error) { + Logger.error( + `BTCPay webhook processing failed: ${error.message}`, + 'WebhooksService', + ); + if (error instanceof Error) throw error; + throw new Error(`Unknown Error: ${JSON.stringify(error)}`); + } + } + + case 'InvoiceExpired': + case 'InvoiceInvalid': { + Logger.log( + `Invoice ${event.invoiceId} ${event.type.toLowerCase()} — no action needed`, + 'WebhooksService', + ); + break; + } + + default: { + Logger.log( + `Unhandled BTCPay event type: ${event.type}`, + 'WebhooksService', + ); + } + } + } +} diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..824961c --- /dev/null +++ b/backend/test/app.e2e-spec.ts @@ -0,0 +1,29 @@ +// eslint-disable-next-line unicorn/prevent-abbreviations +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppController } from './../src/app.controller'; +import { AppService } from './../src/app.service'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect((response) => + expect(response.text).toMatch(/App has been running for/), + ); + }); +}); diff --git a/backend/test/jest-e2e.json b/backend/test/jest-e2e.json new file mode 100644 index 0000000..884afef --- /dev/null +++ b/backend/test/jest-e2e.json @@ -0,0 +1,13 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": "..", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "moduleNameMapper": { + "^src/(.*)$": "/src/$1" + }, + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "setupFilesAfterEnv": ["/test/setup.ts"] +} diff --git a/backend/test/mocks/subscription.ts b/backend/test/mocks/subscription.ts new file mode 100644 index 0000000..5864495 --- /dev/null +++ b/backend/test/mocks/subscription.ts @@ -0,0 +1,54 @@ +import { Subscription } from 'src/subscriptions/entities/subscription.entity'; +import { SubscriptionPeriod } from 'src/subscriptions/enums/periods.enum'; +import { PaymentStatus } from 'src/subscriptions/enums/status.enum'; +import { SubscriptionType } from 'src/subscriptions/enums/types.enum'; +import { createUser } from './user'; +import { User } from 'src/users/entities/user.entity'; + +type SubscriptionParameters = { + type?: SubscriptionType; + period?: SubscriptionPeriod; + status?: PaymentStatus; + createdAt?: Date; + user?: User; +}; + +export const createSubscription = ({ + type = 'enthusiast', + period = 'monthly', + status = 'succeeded', + createdAt = new Date(), + user, +}: SubscriptionParameters): Subscription => ({ + id: 'ab2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + stripeId: 'sub_123', + status, + type, + period, + userId: 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + user, + createdAt, + flashEvents: [], +}); + +export const audienceMonthlyStripe = createSubscription({ + type: 'enthusiast', + period: 'monthly', + user: createUser(), +}); + +export const rssAddonMonthlyStripe = createSubscription({ + type: 'rss-addon', + period: 'monthly', +}); + +export const verificationAddonMonthlyStripe = createSubscription({ + type: 'verification-addon', + period: 'monthly', +}); + +export const audienceMonthlyRejectedStripe = createSubscription({ + type: 'enthusiast', + period: 'monthly', + status: 'rejected', +}); diff --git a/backend/test/mocks/user.ts b/backend/test/mocks/user.ts new file mode 100644 index 0000000..e1cb7f0 --- /dev/null +++ b/backend/test/mocks/user.ts @@ -0,0 +1,82 @@ +import { User } from 'src/users/entities/user.entity'; +import { + audienceMonthlyStripe, + rssAddonMonthlyStripe, + verificationAddonMonthlyStripe, +} from './subscription'; +import { Subscription } from 'src/subscriptions/entities/subscription.entity'; +import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity'; + +interface UserParameters { + id?: string; + email?: string; + legalName?: string; + profilePictureUrl?: string; + subscriptions?: Subscription[]; + filmmaker?: Filmmaker; +} +export const createUser = ({ + id = 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + email = 'test@oneseventech.com', + legalName = 'Test User', + profilePictureUrl = 'https://example.com/profile.jpg', + subscriptions = [], + filmmaker, +}: UserParameters = {}): User => { + return { + cognitoId: 'test-cognito-id', + id, + email, + legalName, + profilePictureUrl, + createdAt: new Date(), + updatedAt: new Date(), + subscriptions, + filmmaker, + seasonRents: [], + redemptions: [], + }; +}; + +const createFilmmaker = ( + id: string, + userId: string, + professionalName: string, + bio: string, +): any => { + return { + id, + userId, + professionalName, + lightningAddress: '', + bio, + createdAt: new Date(), + updatedAt: new Date(), + }; +}; + +export const audienceUserStripe: User = createUser({ + subscriptions: [audienceMonthlyStripe], +}); + +export const proPlusUserStripe: User = createUser({ + subscriptions: [rssAddonMonthlyStripe], + filmmaker: createFilmmaker( + 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + 'Test Filmmaker', + 'Test bio', + ), +}); + +export const ultimateUserStripe: User = createUser({ + subscriptions: [verificationAddonMonthlyStripe], + filmmaker: createFilmmaker( + 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5', + 'Test Filmmaker', + 'Test bio', + ), +}); + +export const audienceUserNoSubscription: User = createUser(); diff --git a/backend/test/nostr-auth.e2e-spec.ts b/backend/test/nostr-auth.e2e-spec.ts new file mode 100644 index 0000000..1afdff4 --- /dev/null +++ b/backend/test/nostr-auth.e2e-spec.ts @@ -0,0 +1,97 @@ +/* eslint-disable unicorn/prevent-abbreviations */ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as request from 'supertest'; +import { createHash } from 'node:crypto'; +import { + finalizeEvent, + generateSecretKey, + getPublicKey, + type UnsignedEvent, +} from 'nostr-tools'; +import { NostrAuthModule } from '../src/nostr-auth/nostr-auth.module'; + +const hashPayload = (payload: string) => + createHash('sha256').update(payload).digest('hex'); + +describe('NostrAuth (e2e)', () => { + let app: INestApplication; + + const secretKey = generateSecretKey(); + const pubkey = getPublicKey(secretKey); + const host = 'nostr.test'; + const path = '/nostr-auth/echo'; + const url = `http://${host}${path}`; + + const buildAuthHeader = (unsignedEvent: UnsignedEvent): string => { + const event = finalizeEvent(unsignedEvent, secretKey); + return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`; + }; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [NostrAuthModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('accepts a valid nostr-signed request', async () => { + const body = { ping: 'pong' }; + const payload = JSON.stringify(body); + + const authHeader = buildAuthHeader({ + pubkey, + kind: 27_235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(payload)], + ], + content: '', + }); + + const response = await request(app.getHttpServer()) + .post(path) + .set('host', host) + .set('x-forwarded-proto', 'http') + .set('authorization', authHeader) + .send(body) + .expect(201); + + expect(response.body.pubkey).toBe(pubkey); + }); + + it('rejects a tampered payload hash', async () => { + const body = { ping: 'pong' }; + const payload = JSON.stringify(body); + + const authHeader = buildAuthHeader({ + pubkey, + kind: 27_235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(`${payload}tampered`)], + ], + content: '', + }); + + const response = await request(app.getHttpServer()) + .post(path) + .set('host', host) + .set('x-forwarded-proto', 'http') + .set('authorization', authHeader) + .send(body) + .expect(401); + + expect(response.body.code).toBe('PAYLOAD_MISMATCH'); + }); +}); diff --git a/backend/test/nostr-session.e2e-spec.ts b/backend/test/nostr-session.e2e-spec.ts new file mode 100644 index 0000000..f445eaa --- /dev/null +++ b/backend/test/nostr-session.e2e-spec.ts @@ -0,0 +1,108 @@ +/* eslint-disable unicorn/no-useless-undefined */ +/* eslint-disable unicorn/prevent-abbreviations */ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as request from 'supertest'; +import { createHash } from 'node:crypto'; +import { + finalizeEvent, + generateSecretKey, + getPublicKey, + type UnsignedEvent, +} from 'nostr-tools'; +import { AuthController } from '../src/auth/auth.controller'; +import { AuthService } from '../src/auth/auth.service'; +import { UsersService } from '../src/users/users.service'; +import { FilmmakersService } from '../src/filmmakers/filmmakers.service'; +import { NostrAuthGuard } from '../src/nostr-auth/nostr-auth.guard'; +import { NostrAuthService } from '../src/nostr-auth/nostr-auth.service'; +import { NostrSessionService } from '../src/auth/nostr-session.service'; +import { NostrSessionJwtGuard } from '../src/auth/guards/nostr-session-jwt.guard'; + +const hashPayload = (payload: string) => + createHash('sha256').update(payload).digest('hex'); + +describe('Nostr session bridge (e2e)', () => { + let app: INestApplication; + + const secretKey = generateSecretKey(); + const pubkey = getPublicKey(secretKey); + const host = 'nostr.test'; + const path = '/auth/nostr/session'; + const url = `http://${host}${path}`; + + const buildAuthHeader = (unsignedEvent: UnsignedEvent): string => { + const event = finalizeEvent(unsignedEvent, secretKey); + return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`; + }; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + NostrSessionService, + NostrAuthService, + NostrAuthGuard, + NostrSessionJwtGuard, + { provide: AuthService, useValue: {} }, + { + provide: UsersService, + useValue: { + findUserByNostrPubkey: jest.fn().mockResolvedValue(undefined), + findUsersById: jest.fn().mockResolvedValue(undefined), + }, + }, + { provide: FilmmakersService, useValue: {} }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('issues, refreshes, and accepts nostr session JWTs', async () => { + const body = { ping: 'pong' }; + const payload = JSON.stringify(body); + + const authHeader = buildAuthHeader({ + pubkey, + kind: 27_235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['u', url], + ['method', 'POST'], + ['payload', hashPayload(payload)], + ], + content: '', + }); + + const createResponse = await request(app.getHttpServer()) + .post(path) + .set('host', host) + .set('x-forwarded-proto', 'http') + .set('authorization', authHeader) + .send(body) + .expect(201); + + const { accessToken, refreshToken } = createResponse.body; + expect(accessToken).toBeDefined(); + expect(refreshToken).toBeDefined(); + + const refreshResponse = await request(app.getHttpServer()) + .post('/auth/nostr/refresh') + .send({ refreshToken }) + .expect(201); + + expect(refreshResponse.body.accessToken).toBeDefined(); + + await request(app.getHttpServer()) + .post('/auth/nostr/logout') + .set('authorization', `Bearer ${accessToken}`) + .expect(201) + .expect((res) => expect(res.body.success).toBe(true)); + }); +}); diff --git a/backend/test/setup.ts b/backend/test/setup.ts new file mode 100644 index 0000000..556e412 --- /dev/null +++ b/backend/test/setup.ts @@ -0,0 +1,65 @@ +process.env.ENVIRONMENT = 'development'; +process.env.PORT = '4000'; +process.env.DOMAIN = 'localhost:4000'; + +process.env.DATABASE_HOST = 'rds.com'; +process.env.DATABASE_PORT = '5432'; +process.env.DATABASE_USER = 'postgres'; +process.env.DATABASE_PASSWORD = 'password'; +process.env.DATABASE_NAME = 'indeehub'; + +process.env.ZBD_API_KEY = 'api-key'; +process.env.STRIKE_API_KEY = 'api-key'; +process.env.STRIKE_WEBHOOK_SECRET = 'api-key'; + +process.env.COGNITO_USER_POOL_ID = 'pool-id'; +process.env.COGNITO_CLIENT_ID = 'client-id'; + +process.env.SENDGRID_API_KEY = 'sendgrid-api-key'; +process.env.SENDGRID_SENDER = 'hello@indeehub.studio'; +process.env.SENDGRID_WAITLIST = 'id'; + +process.env.AWS_ACCESS_KEY = 'access-key'; +process.env.AWS_SECRET_KEY = 'secret-key'; +process.env.AWS_REGION = 'us-west-1'; + +process.env.S3_PUBLIC_BUCKET_URL = 'https://public.cloudfront.net/'; +process.env.S3_PUBLIC_BUCKET_NAME = 'public'; + +process.env.S3_PRIVATE_BUCKET_URL = 'https://private.cloudfront.net/'; +process.env.S3_PRIVATE_BUCKET_NAME = 'private'; + +process.env.CLOUDFRONT_PRIVATE_KEY = 'key'; +process.env.CLOUDFRONT_KEY_PAIR_ID = 'key-pair-id'; + +process.env.STRIPE_SECRET_KEY = 'sk_test_key'; + +process.env.STRIPE_AUDIENCE_PRODUCT_ID = 'prod_id_audience'; +process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID = 'price_id_monthly'; +process.env.STRIPE_AUDIENCE_YEARLY_PRICE_ID = 'price_id_yearly'; +process.env.STRIPE_PRO_PLUS_PRODUCT_ID = 'prod_id_pro_plus'; +process.env.STRIPE_PRO_PLUS_MONTHLY_PRICE_ID = 'price_id_monthly'; +process.env.STRIPE_PRO_PLUS_YEARLY_PRICE_ID = 'price_id_yearly'; +process.env.STRIPE_ULTIMATE_PRODUCT_ID = 'prod_id_ultimate'; +process.env.STRIPE_ULTIMATE_MONTHLY_PRICE_ID = 'price_id_monthly'; +process.env.STRIPE_ULTIMATE_YEARLY_PRICE_ID = 'price_id_yearly'; + +process.env.STRIPE_WEBHOOK_KEY = 'whsec_key'; + +process.env.FLASH_JWT_SECRET_AUDIENCE = 'flash_audience_secret'; +process.env.FLASH_JWT_SECRET_PRO_PLUS = 'flash_pro_plus_secret'; +process.env.FLASH_JWT_SECRET_ULTIMATE = 'flash_ultimate_secret'; + +process.env.FRONTEND_URL = 'http://localhost:3000'; + +process.env.TRANSCODING_API_KEY = 'api-key'; +process.env.NOSTR_JWT_SECRET = 'nostr-jwt-secret'; +process.env.NOSTR_JWT_REFRESH_SECRET = 'nostr-jwt-refresh-secret'; + +process.env.QUEUE_HOST = 'redis.com'; +process.env.QUEUE_PORT = '6379'; +process.env.QUEUE_PASSWORD = 'password'; + +process.env.PODPING_URL = 'https://podping.cloud/'; +process.env.PODPING_KEY = 'podping-key'; +process.env.PODPING_USER_AGENT = 'Indeehub'; diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..cc47eb8 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "inlineSources": true, + "sourceRoot": "/" + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 84abba5..cecd48b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - "7777:7777" depends_on: - relay + - api networks: - indeedhub-network labels: @@ -22,6 +23,118 @@ services: retries: 3 start_period: 40s + # ── Backend API (NestJS) ───────────────────────────────────── + api: + build: + context: ./backend + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + networks: + - indeedhub-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/nostr-auth/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + + # ── PostgreSQL Database ────────────────────────────────────── + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: indeedhub + POSTGRES_PASSWORD: indeedhub_dev_2026 + POSTGRES_DB: indeedhub + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - indeedhub-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U indeedhub"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # ── Redis (BullMQ job queue) ───────────────────────────────── + redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - redis-data:/data + networks: + - indeedhub-network + + # ── MinIO (S3-compatible object storage) ───────────────────── + minio: + image: minio/minio:latest + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + volumes: + - minio-data:/data + ports: + - "9001:9001" + networks: + - indeedhub-network + + # ── MinIO bucket init (one-shot: creates required buckets) ─── + minio-init: + image: minio/mc:latest + depends_on: + - minio + entrypoint: > + /bin/sh -c " + sleep 5; + mc alias set local http://minio:9000 minioadmin minioadmin123; + mc mb local/indeedhub-private --ignore-existing; + mc mb local/indeedhub-public --ignore-existing; + mc anonymous set download local/indeedhub-public; + echo 'MinIO buckets initialized'; + " + networks: + - indeedhub-network + restart: "no" + + # ── FFmpeg Transcoding Worker ──────────────────────────────── + ffmpeg-worker: + build: + context: ./backend + dockerfile: Dockerfile.ffmpeg + restart: unless-stopped + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + networks: + - indeedhub-network + + # ── Mailpit (development email testing) ────────────────────── + mailpit: + image: axllent/mailpit:latest + restart: unless-stopped + ports: + - "8025:8025" + networks: + - indeedhub-network + # ── Nostr Relay (stores comments, reactions, profiles) ─────── relay: image: scsibug/nostr-rs-relay:latest @@ -31,8 +144,7 @@ services: networks: - indeedhub-network - # ── Seeder (one-shot: seeds test data into relay, then exits) ─ - # wait-for-relay.mjs handles readiness polling before seeding. + # ── Seeder (one-shot: seeds test data into relay, then exits) seeder: build: context: . @@ -46,9 +158,29 @@ services: - indeedhub-network restart: "no" + # ── DB Seeder (one-shot: seeds content into PostgreSQL) ────── + db-seeder: + build: + context: ./backend + dockerfile: Dockerfile + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_started + env_file: + - ./backend/.env + command: ["node", "dist/scripts/seed-content.js"] + networks: + - indeedhub-network + restart: "no" + networks: indeedhub-network: driver: bridge volumes: + postgres-data: + redis-data: + minio-data: relay-data: diff --git a/nginx.conf b/nginx.conf index 6df0653..8bc1a01 100644 --- a/nginx.conf +++ b/nginx.conf @@ -8,7 +8,7 @@ server { gzip on; gzip_vary on; gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/vnd.apple.mpegurl video/MP2T; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; @@ -27,7 +27,59 @@ server { add_header Cache-Control "public, immutable"; } - # WebSocket proxy to Nostr relay (Docker service) + # ── Backend API proxy ────────────────────────────────────── + location /api/ { + resolver 127.0.0.11 valid=30s ipv6=off; + set $api_upstream http://api:4000; + + rewrite ^/api(.*) $1 break; + proxy_pass $api_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + + # Handle large uploads + client_max_body_size 100m; + } + + # ── MinIO storage proxy (public bucket) ──────────────────── + # Serves poster images, HLS segments, etc. with caching + location /storage/ { + resolver 127.0.0.11 valid=30s ipv6=off; + set $minio_upstream http://minio:9000; + + rewrite ^/storage/(.*) /indeedhub-public/$1 break; + proxy_pass $minio_upstream; + proxy_http_version 1.1; + proxy_set_header Host minio:9000; + + # Cache static assets aggressively + proxy_cache_valid 200 1d; + proxy_cache_valid 404 1m; + expires 1d; + add_header Cache-Control "public, max-age=86400"; + add_header X-Cache-Status $upstream_cache_status; + } + + # ── MinIO storage proxy (private bucket -- for HLS key delivery) ─ + location /storage-private/ { + resolver 127.0.0.11 valid=30s ipv6=off; + set $minio_upstream http://minio:9000; + + rewrite ^/storage-private/(.*) /indeedhub-private/$1 break; + proxy_pass $minio_upstream; + proxy_http_version 1.1; + proxy_set_header Host minio:9000; + + # Do NOT cache private content + add_header Cache-Control "no-store"; + } + + # ── WebSocket proxy to Nostr relay (Docker service) ──────── location /relay { resolver 127.0.0.11 valid=30s ipv6=off; set $relay_upstream http://relay:8080; @@ -43,7 +95,7 @@ server { proxy_send_timeout 86400s; } - # Vue Router - SPA fallback + # ── Vue Router - SPA fallback ────────────────────────────── location / { try_files $uri $uri/ /index.html; } diff --git a/package-lock.json b/package-lock.json index 1b73262..aa6e630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,14 @@ "axios": "^1.13.5", "nostr-tools": "^2.23.0", "pinia": "^3.0.4", + "qrcode": "^1.5.4", "vue": "^3.5.24", "vue-router": "^4.6.3", "ws": "^8.19.0" }, "devDependencies": { "@types/node": "^24.10.0", + "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", "@vitejs/plugin-vue": "^6.0.1", "@vue/tsconfig": "^0.8.1", @@ -3319,11 +3321,20 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -4483,6 +4494,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -4552,11 +4572,91 @@ "node": ">= 6" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4569,7 +4669,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4755,6 +4854,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -4827,6 +4935,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -5203,6 +5317,19 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -5377,6 +5504,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5818,7 +5954,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6287,6 +6422,18 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -6584,6 +6731,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6598,6 +6781,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6708,6 +6900,15 @@ "node": ">= 6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6910,6 +7111,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7072,6 +7290,15 @@ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7082,6 +7309,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -7304,6 +7537,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8648,6 +8887,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -9106,12 +9351,94 @@ } } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index d6f63fe..df06b4f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "type": "module", "scripts": { "dev": "vite --port 5174", + "dev:full": "bash scripts/dev.sh", + "dev:full:no-docker": "bash scripts/dev.sh --no-docker", "start": "bash scripts/start.sh", "build": "vue-tsc -b && vite build", "preview": "vite preview", @@ -24,12 +26,14 @@ "axios": "^1.13.5", "nostr-tools": "^2.23.0", "pinia": "^3.0.4", + "qrcode": "^1.5.4", "vue": "^3.5.24", "vue-router": "^4.6.3", "ws": "^8.19.0" }, "devDependencies": { "@types/node": "^24.10.0", + "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", "@vitejs/plugin-vue": "^6.0.1", "@vue/tsconfig": "^0.8.1", diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..d68f4b4 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# IndeeHub — Full-stack development launcher +# +# Starts everything you need for local development: +# 1. Infrastructure (Postgres, Redis, MinIO) — Homebrew or Docker +# 2. Local Nostr relay (nak) — optional +# 3. Backend API (NestJS on port 4000) +# 4. Frontend dev server (Vite on port 5174) +# +# Usage: +# bash scripts/dev.sh # start everything +# bash scripts/dev.sh --no-docker # skip Docker (use Homebrew services) +# bash scripts/dev.sh --no-seed # skip Nostr relay seeding +# +# Press Ctrl+C to stop everything cleanly. +# ────────────────────────────────────────────────────────────── +set -e + +# ── Configuration ──────────────────────────────────────────── +RELAY_PORT=7777 +RELAY_URL="ws://localhost:$RELAY_PORT" +VITE_PORT=5174 +BACKEND_PORT=4000 +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" + +SKIP_DOCKER=false +SKIP_SEED=false + +for arg in "$@"; do + case "$arg" in + --no-docker) SKIP_DOCKER=true ;; + --no-seed) SKIP_SEED=true ;; + esac +done + +# ── Colours ────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Colour + +# ── PID tracking for cleanup ──────────────────────────────── +PIDS=() +MINIO_PID="" + +cleanup() { + echo "" + echo -e "${YELLOW}Shutting down all services...${NC}" + + for pid in "${PIDS[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null + fi + done + + # Stop MinIO if we started it + if [ -n "$MINIO_PID" ] && kill -0 "$MINIO_PID" 2>/dev/null; then + kill "$MINIO_PID" 2>/dev/null + fi + + # Stop Docker infrastructure (if we used Docker) + if [ "$SKIP_DOCKER" = false ] && command -v docker &>/dev/null; then + echo -e "${CYAN}Stopping Docker services...${NC}" + docker compose -f "$ROOT_DIR/docker-compose.yml" stop postgres redis minio minio-init mailpit 2>/dev/null || true + fi + + # Kill anything still on relay port + lsof -ti :$RELAY_PORT 2>/dev/null | xargs kill -9 2>/dev/null || true + + echo -e "${GREEN}All services stopped.${NC}" + exit 0 +} +trap cleanup SIGINT SIGTERM EXIT + +# ── Prefixed output helper ─────────────────────────────────── +prefix_output() { + local label="$1" + local colour="$2" + while IFS= read -r line; do + echo -e "${colour}[${label}]${NC} $line" + done +} + +# ── Pre-flight checks ─────────────────────────────────────── +echo -e "${BOLD}${CYAN}" +echo "╔══════════════════════════════════════════╗" +echo "║ IndeeHub Dev Environment ║" +echo "╚══════════════════════════════════════════╝" +echo -e "${NC}" + +# Check required tools +MISSING=() +command -v node &>/dev/null || MISSING+=("node") +command -v npm &>/dev/null || MISSING+=("npm") + +if [ ${#MISSING[@]} -gt 0 ]; then + echo -e "${RED}Missing required tools: ${MISSING[*]}${NC}" + echo "Install them and try again." + exit 1 +fi + +HAS_NAK=true +if ! command -v nak &>/dev/null; then + HAS_NAK=false + echo -e "${YELLOW}Warning: 'nak' not installed. Skipping local Nostr relay.${NC}" + echo -e "${YELLOW}Install with: brew install nak${NC}" + echo "" +fi + +HAS_DOCKER=false +if command -v docker &>/dev/null; then + HAS_DOCKER=true +fi + +# ═════════════════════════════════════════════════════════════ +# STEP 1 — Infrastructure (Homebrew or Docker) +# ═════════════════════════════════════════════════════════════ +if [ "$SKIP_DOCKER" = true ] || [ "$HAS_DOCKER" = false ]; then + echo -e "${CYAN}[1/4] Starting infrastructure via Homebrew...${NC}" + + # Postgres + if command -v pg_isready &>/dev/null || [ -f /opt/homebrew/opt/postgresql@15/bin/pg_isready ]; then + PG_ISREADY="${PG_ISREADY:-$(command -v pg_isready 2>/dev/null || echo /opt/homebrew/opt/postgresql@15/bin/pg_isready)}" + if $PG_ISREADY -h localhost -p 5432 -q 2>/dev/null; then + echo -e "${GREEN} Postgres is already running.${NC}" + else + echo -e "${CYAN} Starting Postgres...${NC}" + brew services start postgresql@15 2>/dev/null || true + sleep 2 + fi + else + echo -e "${RED} Postgres not found. Install with: brew install postgresql@15${NC}" + exit 1 + fi + + # Redis + if command -v redis-cli &>/dev/null; then + if redis-cli ping 2>/dev/null | grep -q PONG; then + echo -e "${GREEN} Redis is already running.${NC}" + else + echo -e "${CYAN} Starting Redis...${NC}" + brew services start redis 2>/dev/null || true + sleep 1 + fi + else + echo -e "${RED} Redis not found. Install with: brew install redis${NC}" + exit 1 + fi + + # MinIO + if command -v minio &>/dev/null; then + if curl -s -o /dev/null http://localhost:9000/minio/health/live 2>/dev/null; then + echo -e "${GREEN} MinIO is already running.${NC}" + else + echo -e "${CYAN} Starting MinIO...${NC}" + mkdir -p /tmp/minio-data + MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=minioadmin123 \ + minio server /tmp/minio-data --console-address ":9001" --address ":9000" \ + > /tmp/minio.log 2>&1 & + MINIO_PID=$! + sleep 2 + + # Create buckets if mc is available + if command -v mc &>/dev/null; then + mc alias set local http://localhost:9000 minioadmin minioadmin123 2>/dev/null || true + mc mb local/indeedhub-private --ignore-existing 2>/dev/null || true + mc mb local/indeedhub-public --ignore-existing 2>/dev/null || true + mc anonymous set download local/indeedhub-public 2>/dev/null || true + echo -e "${GREEN} MinIO buckets configured.${NC}" + fi + fi + else + echo -e "${YELLOW} MinIO not found (optional). Install with: brew install minio/stable/minio${NC}" + fi + + # Ensure the Postgres database exists + PSQL="${PSQL:-$(command -v psql 2>/dev/null || echo /opt/homebrew/opt/postgresql@15/bin/psql)}" + CREATEDB="${CREATEDB:-$(command -v createdb 2>/dev/null || echo /opt/homebrew/opt/postgresql@15/bin/createdb)}" + CREATEUSER="${CREATEUSER:-$(command -v createuser 2>/dev/null || echo /opt/homebrew/opt/postgresql@15/bin/createuser)}" + + # Create user if not exists + if ! $PSQL -U "$(whoami)" -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='indeedhub'" 2>/dev/null | grep -q 1; then + $CREATEUSER -U "$(whoami)" --superuser indeedhub 2>/dev/null || true + $PSQL -U "$(whoami)" -d postgres -c "ALTER USER indeedhub WITH PASSWORD 'indeedhub_dev_2026';" 2>/dev/null || true + fi + + # Create database if not exists + if ! $PSQL -U "$(whoami)" -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='indeedhub'" 2>/dev/null | grep -q 1; then + $CREATEDB -U "$(whoami)" -O indeedhub indeedhub 2>/dev/null || true + fi + + echo -e "${GREEN} Homebrew infrastructure is ready.${NC}" + +else + echo -e "${CYAN}[1/4] Starting Docker infrastructure...${NC}" + + docker compose -f "$ROOT_DIR/docker-compose.yml" up -d \ + postgres redis minio minio-init mailpit 2>&1 | prefix_output "docker" "$CYAN" + + echo -e "${CYAN} Waiting for Postgres...${NC}" + for i in $(seq 1 30); do + if docker compose -f "$ROOT_DIR/docker-compose.yml" exec -T postgres pg_isready -U indeedhub -q 2>/dev/null; then + echo -e "${GREEN} Postgres is ready.${NC}" + break + fi + if [ "$i" -eq 30 ]; then + echo -e "${RED} Postgres did not become healthy in time.${NC}" + exit 1 + fi + sleep 1 + done + + echo -e "${GREEN} Docker infrastructure is up.${NC}" +fi + +# ═════════════════════════════════════════════════════════════ +# STEP 2 — Local Nostr relay +# ═════════════════════════════════════════════════════════════ +if [ "$HAS_NAK" = true ]; then + echo -e "${CYAN}[2/4] Starting local Nostr relay on port $RELAY_PORT...${NC}" + + # Kill existing process on the port + if lsof -i :$RELAY_PORT -P &>/dev/null; then + lsof -ti :$RELAY_PORT | xargs kill -9 2>/dev/null || true + sleep 1 + fi + + nak serve --port $RELAY_PORT > /dev/null 2>&1 & + PIDS+=($!) + + # Wait for relay + for i in $(seq 1 20); do + if curl -s -o /dev/null http://localhost:$RELAY_PORT 2>/dev/null; then + echo -e "${GREEN} Relay is ready at $RELAY_URL${NC}" + break + fi + sleep 0.5 + done + + # Seed relay + if [ "$SKIP_SEED" = false ]; then + if [ -f "$ROOT_DIR/scripts/seed-profiles.ts" ]; then + echo -e "${CYAN} Seeding test profiles...${NC}" + (cd "$ROOT_DIR" && npx tsx scripts/seed-profiles.ts 2>&1 | prefix_output "seed" "$CYAN") || true + fi + if [ -f "$ROOT_DIR/scripts/seed-activity.ts" ]; then + echo -e "${CYAN} Seeding activity...${NC}" + (cd "$ROOT_DIR" && npx tsx scripts/seed-activity.ts 2>&1 | prefix_output "seed" "$CYAN") || true + fi + fi +else + echo -e "${YELLOW}[2/4] Skipping Nostr relay (nak not installed).${NC}" +fi + +# ═════════════════════════════════════════════════════════════ +# STEP 3 — Backend API +# ═════════════════════════════════════════════════════════════ +echo -e "${CYAN}[3/4] Starting backend API on port $BACKEND_PORT...${NC}" + +# Override Docker service hostnames → localhost so the backend +# can reach services from the host machine. +export DATABASE_HOST=localhost +export QUEUE_HOST=localhost +export S3_ENDPOINT=http://localhost:9000 +export SMTP_HOST=localhost + +(cd "$BACKEND_DIR" && npm run start:dev 2>&1 | prefix_output "api" "$GREEN") & +PIDS+=($!) + +# Wait for the API to respond +echo -e "${CYAN} Waiting for API...${NC}" +for i in $(seq 1 60); do + if curl -s -o /dev/null "http://localhost:$BACKEND_PORT" 2>/dev/null; then + echo -e "${GREEN} Backend API is ready at http://localhost:$BACKEND_PORT${NC}" + break + fi + if [ "$i" -eq 60 ]; then + echo -e "${YELLOW} Backend is still starting (check logs above for errors).${NC}" + fi + sleep 2 +done + +# ═════════════════════════════════════════════════════════════ +# STEP 4 — Frontend dev server +# ═════════════════════════════════════════════════════════════ +echo "" +echo -e "${BOLD}${GREEN}" +echo "════════════════════════════════════════════" +echo " All services launching! " +echo "" +echo " Frontend: http://localhost:$VITE_PORT" +echo " Backend: http://localhost:$BACKEND_PORT" +echo " MinIO: http://localhost:9001" +[ "$HAS_NAK" = true ] && echo " Relay: $RELAY_URL" +echo "" +echo " Mock mode: OFF (VITE_USE_MOCK_DATA=false)" +echo " BTCPay: ${BTCPAY_URL:-from backend/.env}" +echo "" +echo " Press Ctrl+C to stop everything" +echo "════════════════════════════════════════════" +echo -e "${NC}" + +# Run Vite in the foreground so Ctrl+C propagates cleanly +cd "$ROOT_DIR" +npx vite --port $VITE_PORT 2>&1 | prefix_output "vite" "$YELLOW" diff --git a/src/App.vue b/src/App.vue index 1e0423c..fa10298 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,11 @@ diff --git a/src/components/backstage/ContentTab.vue b/src/components/backstage/ContentTab.vue new file mode 100644 index 0000000..88b0e45 --- /dev/null +++ b/src/components/backstage/ContentTab.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/components/backstage/CouponsTab.vue b/src/components/backstage/CouponsTab.vue new file mode 100644 index 0000000..41b06ef --- /dev/null +++ b/src/components/backstage/CouponsTab.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/components/backstage/DetailsTab.vue b/src/components/backstage/DetailsTab.vue new file mode 100644 index 0000000..ecb76fc --- /dev/null +++ b/src/components/backstage/DetailsTab.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/src/components/backstage/DocumentationTab.vue b/src/components/backstage/DocumentationTab.vue new file mode 100644 index 0000000..2c4da60 --- /dev/null +++ b/src/components/backstage/DocumentationTab.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/components/backstage/PermissionsTab.vue b/src/components/backstage/PermissionsTab.vue new file mode 100644 index 0000000..5c13408 --- /dev/null +++ b/src/components/backstage/PermissionsTab.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/backstage/RevenueTab.vue b/src/components/backstage/RevenueTab.vue new file mode 100644 index 0000000..0dc1e3a --- /dev/null +++ b/src/components/backstage/RevenueTab.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/components/backstage/UploadZone.vue b/src/components/backstage/UploadZone.vue new file mode 100644 index 0000000..0e1833a --- /dev/null +++ b/src/components/backstage/UploadZone.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/src/composables/useAccess.ts b/src/composables/useAccess.ts index 85883da..6b4fbde 100644 --- a/src/composables/useAccess.ts +++ b/src/composables/useAccess.ts @@ -2,6 +2,7 @@ import { computed } from 'vue' import { libraryService } from '../services/library.service' import { subscriptionService } from '../services/subscription.service' import { useAuthStore } from '../stores/auth' +import { USE_MOCK } from '../utils/mock' /** * Access Control Composable @@ -27,10 +28,7 @@ export function useAccess() { return { hasAccess: true, method: 'subscription' } } - // Check if we're in development mode - const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV - - if (useMockData) { + if (USE_MOCK) { // In dev mode without subscription, no access (prompt rental) return { hasAccess: false } } diff --git a/src/composables/useAccounts.ts b/src/composables/useAccounts.ts index e923789..f08e6a8 100644 --- a/src/composables/useAccounts.ts +++ b/src/composables/useAccounts.ts @@ -131,14 +131,18 @@ export function useAccounts() { } /** - * Logout current account + * Logout current account. + * removeAccount already clears the active account if it's the one being removed. */ function logout() { const current = accountManager.active if (current) { - accountManager.removeAccount(current) + try { + accountManager.removeAccount(current) + } catch (err) { + console.debug('Account removal cleanup:', err) + } } - accountManager.setActive(null as any) } /** diff --git a/src/composables/useFilmmaker.ts b/src/composables/useFilmmaker.ts new file mode 100644 index 0000000..7fa3e78 --- /dev/null +++ b/src/composables/useFilmmaker.ts @@ -0,0 +1,340 @@ +/** + * useFilmmaker composable + * + * Reactive state and actions for the filmmaker/creator dashboard. + * In dev mode without a backend, uses local mock data so the + * backstage UI is fully functional for prototyping. + */ + +import { ref, computed } from 'vue' +import { filmmakerService } from '../services/filmmaker.service' +import type { + ApiProject, + ApiFilmmakerAnalytics, + ApiWatchAnalytics, + ApiPaymentMethod, + ApiPayment, + ApiGenre, + CreateProjectData, + UpdateProjectData, + ProjectType, +} from '../types/api' +import { USE_MOCK } from '../utils/mock' + +// ── Shared reactive state (singleton across components) ───────────────────────── +const projects = ref([]) +const projectsCount = ref(0) +const isLoading = ref(false) +const error = ref(null) +const genres = ref([]) + +// Analytics state +const analytics = ref(null) +const watchAnalytics = ref(null) + +// Payment state +const paymentMethods = ref([]) +const payments = ref([]) + +// Filters +const activeTab = ref<'projects' | 'resume' | 'stakeholder'>('projects') +const typeFilter = ref(null) +const sortBy = ref<'a-z' | 'z-a' | 'recent'>('recent') +const searchQuery = ref('') + +// ── Mock data helpers ──────────────────────────────────────────────────────────── + +let mockIdCounter = 1 + +function createMockProject(data: CreateProjectData): ApiProject { + const id = `mock-project-${mockIdCounter++}` + const now = new Date().toISOString() + return { + id, + name: data.name, + title: data.name, + type: data.type, + status: 'draft', + slug: data.name.toLowerCase().replace(/\s+/g, '-'), + createdAt: now, + updatedAt: now, + } as ApiProject +} + +const MOCK_GENRES: ApiGenre[] = [ + { id: '1', name: 'Documentary', slug: 'documentary' }, + { id: '2', name: 'Drama', slug: 'drama' }, + { id: '3', name: 'Thriller', slug: 'thriller' }, + { id: '4', name: 'Comedy', slug: 'comedy' }, + { id: '5', name: 'Sci-Fi', slug: 'sci-fi' }, + { id: '6', name: 'Animation', slug: 'animation' }, + { id: '7', name: 'Horror', slug: 'horror' }, + { id: '8', name: 'Action', slug: 'action' }, + { id: '9', name: 'Romance', slug: 'romance' }, + { id: '10', name: 'Music', slug: 'music' }, +] + +// ── Composable ─────────────────────────────────────────────────────────────────── + +export function useFilmmaker() { + /** + * Sorted and filtered projects based on active filters + */ + const filteredProjects = computed(() => { + let result = [...projects.value] + + // Filter by type + if (typeFilter.value) { + result = result.filter((p) => p.type === typeFilter.value) + } + + // Filter by search + if (searchQuery.value.trim()) { + const q = searchQuery.value.toLowerCase() + result = result.filter( + (p) => + p.name?.toLowerCase().includes(q) || + p.title?.toLowerCase().includes(q) + ) + } + + // Sort + if (sortBy.value === 'a-z') { + result.sort((a, b) => (a.title || a.name || '').localeCompare(b.title || b.name || '')) + } else if (sortBy.value === 'z-a') { + result.sort((a, b) => (b.title || b.name || '').localeCompare(a.title || a.name || '')) + } else { + result.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) + } + + return result + }) + + // ── Project Actions ───────────────────────────────────────────────────────── + + async function fetchProjects() { + isLoading.value = true + error.value = null + try { + if (USE_MOCK) { + // Mock mode: projects are already in local state + projectsCount.value = projects.value.length + return + } + const [list, count] = await Promise.all([ + filmmakerService.getPrivateProjects({ + type: typeFilter.value || undefined, + search: searchQuery.value || undefined, + }), + filmmakerService.getPrivateProjectsCount({ + type: typeFilter.value || undefined, + search: searchQuery.value || undefined, + }), + ]) + projects.value = list + projectsCount.value = count + } catch (err: any) { + error.value = err.message || 'Failed to load projects' + console.debug('Failed to load projects:', err) + } finally { + isLoading.value = false + } + } + + async function createNewProject(data: CreateProjectData): Promise { + isLoading.value = true + error.value = null + try { + let project: ApiProject + if (USE_MOCK) { + // Simulate brief delay + await new Promise(resolve => setTimeout(resolve, 300)) + project = createMockProject(data) + } else { + project = await filmmakerService.createProject(data) + } + // Add to local list + projects.value = [project, ...projects.value] + projectsCount.value++ + return project + } catch (err: any) { + error.value = err.message || 'Failed to create project' + console.error('Failed to create project:', err) + return null + } finally { + isLoading.value = false + } + } + + async function saveProject(id: string, data: UpdateProjectData): Promise { + error.value = null + try { + let updated: ApiProject + if (USE_MOCK) { + await new Promise(resolve => setTimeout(resolve, 200)) + const idx = projects.value.findIndex((p) => p.id === id) + if (idx === -1) throw new Error('Project not found') + updated = { ...projects.value[idx], ...data, updatedAt: new Date().toISOString() } as ApiProject + projects.value[idx] = updated + } else { + updated = await filmmakerService.updateProject(id, data) + const idx = projects.value.findIndex((p) => p.id === id) + if (idx !== -1) projects.value[idx] = updated + } + return updated + } catch (err: any) { + error.value = err.message || 'Failed to save project' + console.error('Failed to save project:', err) + return null + } + } + + async function removeProject(id: string): Promise { + error.value = null + try { + if (USE_MOCK) { + await new Promise(resolve => setTimeout(resolve, 200)) + } else { + await filmmakerService.deleteProject(id) + } + projects.value = projects.value.filter((p) => p.id !== id) + projectsCount.value-- + return true + } catch (err: any) { + error.value = err.message || 'Failed to delete project' + console.error('Failed to delete project:', err) + return false + } + } + + // ── Analytics Actions ─────────────────────────────────────────────────────── + + async function fetchAnalytics(projectIds?: string[]) { + isLoading.value = true + error.value = null + try { + if (USE_MOCK) { + analytics.value = { + balance: 125000, + totalEarnings: 450000, + myTotalEarnings: 360000, + averageSharePercentage: 80, + } + watchAnalytics.value = { + viewsByDate: {}, + trailerViews: 3891, + averageWatchTime: 2340, + streamingRevenueSats: 280000, + rentalRevenueSats: 170000, + purchasesCount: 89, + purchasesByContent: [], + revenueByDate: {}, + } + return + } + const [analyticData, watchData] = await Promise.all([ + filmmakerService.getFilmmakerAnalytics(), + filmmakerService.getWatchAnalytics(projectIds), + ]) + analytics.value = analyticData + watchAnalytics.value = watchData + } catch (err: any) { + error.value = err.message || 'Failed to load analytics' + console.error('Failed to load analytics:', err) + } finally { + isLoading.value = false + } + } + + // ── Payment Actions ───────────────────────────────────────────────────────── + + async function fetchPaymentMethods() { + try { + if (USE_MOCK) { + paymentMethods.value = [] + return + } + paymentMethods.value = await filmmakerService.getPaymentMethods() + } catch (err: any) { + console.error('Failed to load payment methods:', err) + } + } + + async function fetchPayments(filmId?: string) { + try { + if (USE_MOCK) { + payments.value = [] + return + } + payments.value = await filmmakerService.getPayments(filmId) + } catch (err: any) { + console.error('Failed to load payments:', err) + } + } + + // ── Single Project Fetch ──────────────────────────────────────────────────── + + async function getProject(id: string): Promise { + error.value = null + try { + if (USE_MOCK) { + // Look up from local projects list + const found = projects.value.find((p) => p.id === id) + return found || null + } + return await filmmakerService.getPrivateProject(id) + } catch (err: any) { + error.value = err.message || 'Failed to load project' + console.error('Failed to load project:', err) + return null + } + } + + // ── Genres ────────────────────────────────────────────────────────────────── + + async function fetchGenres() { + try { + if (USE_MOCK) { + genres.value = MOCK_GENRES + return + } + genres.value = await filmmakerService.getGenres() + } catch (err: any) { + console.error('Failed to load genres:', err) + } + } + + return { + // State + projects, + projectsCount, + filteredProjects, + isLoading, + error, + genres, + analytics, + watchAnalytics, + paymentMethods, + payments, + + // Filters + activeTab, + typeFilter, + sortBy, + searchQuery, + + // Actions + fetchProjects, + createNewProject, + getProject, + saveProject, + removeProject, + fetchAnalytics, + fetchPaymentMethods, + fetchPayments, + fetchGenres, + } +} diff --git a/src/composables/useUpload.ts b/src/composables/useUpload.ts new file mode 100644 index 0000000..5eabdca --- /dev/null +++ b/src/composables/useUpload.ts @@ -0,0 +1,186 @@ +/** + * useUpload composable + * + * Chunked multipart upload with progress tracking. + * Ported from the original indeehub-frontend uploader library. + * Works with both the original API and our self-hosted MinIO backend. + */ + +import { ref, computed } from 'vue' +import { filmmakerService } from '../services/filmmaker.service' +import axios from 'axios' + +const CHUNK_SIZE = 20 * 1024 * 1024 // 20 MB +const MAX_PARALLEL_UPLOADS = 6 +const MAX_RETRIES = 3 + +export interface UploadItem { + id: string + file: File + key: string + bucket: string + progress: number + status: 'pending' | 'uploading' | 'completed' | 'failed' + error?: string +} + +// Shared upload queue (singleton across components) +const uploadQueue = ref([]) +const isUploading = ref(false) + +export function useUpload() { + const totalProgress = computed(() => { + if (uploadQueue.value.length === 0) return 0 + const total = uploadQueue.value.reduce((sum, item) => sum + item.progress, 0) + return Math.round(total / uploadQueue.value.length) + }) + + const activeUploads = computed(() => + uploadQueue.value.filter((u) => u.status === 'uploading') + ) + + const completedUploads = computed(() => + uploadQueue.value.filter((u) => u.status === 'completed') + ) + + /** + * Add a file to the upload queue and start uploading + */ + async function addUpload(file: File, key: string, bucket: string = 'indeedhub-private'): Promise { + const item: UploadItem = { + id: `upload-${Date.now()}-${Math.random().toString(36).slice(2)}`, + file, + key, + bucket, + progress: 0, + status: 'pending', + } + + uploadQueue.value.push(item) + return processUpload(item) + } + + /** + * Process a single upload: initialize, chunk, upload, finalize + */ + async function processUpload(item: UploadItem): Promise { + try { + item.status = 'uploading' + isUploading.value = true + + // Step 1: Initialize multipart upload + const { UploadId, Key } = await filmmakerService.initializeUpload( + item.key, + item.bucket, + item.file.type + ) + + // Step 2: Calculate chunks + const totalChunks = Math.ceil(item.file.size / CHUNK_SIZE) + + // Step 3: Get presigned URLs for all chunks + const { parts: presignedParts } = await filmmakerService.getPresignedUrls( + UploadId, + Key, + item.bucket, + totalChunks + ) + + // Step 4: Upload chunks in parallel with progress tracking + const completedParts: Array<{ PartNumber: number; ETag: string }> = [] + let uploadedChunks = 0 + + // Process chunks in batches of MAX_PARALLEL_UPLOADS + for (let batchStart = 0; batchStart < presignedParts.length; batchStart += MAX_PARALLEL_UPLOADS) { + const batch = presignedParts.slice(batchStart, batchStart + MAX_PARALLEL_UPLOADS) + + const batchResults = await Promise.all( + batch.map(async (part) => { + const start = (part.PartNumber - 1) * CHUNK_SIZE + const end = Math.min(start + CHUNK_SIZE, item.file.size) + const chunk = item.file.slice(start, end) + + // Upload with retries + let lastError: Error | null = null + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + const response = await axios.put(part.signedUrl, chunk, { + headers: { 'Content-Type': item.file.type }, + onUploadProgress: () => { + // Progress is tracked at the chunk level + }, + }) + + uploadedChunks++ + item.progress = Math.round((uploadedChunks / totalChunks) * 100) + + const etag = response.headers.etag || response.headers.ETag + return { + PartNumber: part.PartNumber, + ETag: etag?.replace(/"/g, '') || '', + } + } catch (err: any) { + lastError = err + // Exponential backoff + await new Promise((resolve) => + setTimeout(resolve, Math.pow(2, attempt) * 1000) + ) + } + } + throw lastError || new Error(`Failed to upload part ${part.PartNumber}`) + }) + ) + + completedParts.push(...batchResults) + } + + // Step 5: Finalize + await filmmakerService.finalizeUpload(UploadId, Key, item.bucket, completedParts) + + item.status = 'completed' + item.progress = 100 + + // Check if all uploads done + if (uploadQueue.value.every((u) => u.status !== 'uploading' && u.status !== 'pending')) { + isUploading.value = false + } + + return Key + } catch (err: any) { + item.status = 'failed' + item.error = err.message || 'Upload failed' + console.error('Upload failed:', err) + + if (uploadQueue.value.every((u) => u.status !== 'uploading' && u.status !== 'pending')) { + isUploading.value = false + } + + return null + } + } + + /** + * Remove completed or failed upload from queue + */ + function removeUpload(id: string) { + uploadQueue.value = uploadQueue.value.filter((u) => u.id !== id) + } + + /** + * Clear all completed uploads + */ + function clearCompleted() { + uploadQueue.value = uploadQueue.value.filter((u) => u.status !== 'completed') + } + + return { + uploadQueue, + isUploading, + totalProgress, + activeUploads, + completedUploads, + addUpload, + removeUpload, + clearCompleted, + } +} diff --git a/src/config/api.config.ts b/src/config/api.config.ts index 79339e7..3b9033c 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -12,6 +12,14 @@ export const apiConfig = { retryDelay: 1000, } as const +// IndeeHub self-hosted backend API +// When deployed via Docker, the nginx proxy serves /api/ -> backend:4000 +export const indeehubApiConfig = { + baseURL: import.meta.env.VITE_INDEEHUB_API_URL || '/api', + cdnURL: import.meta.env.VITE_INDEEHUB_CDN_URL || '/storage', + timeout: 30000, +} as const + export const nostrConfig = { relays: (import.meta.env.VITE_NOSTR_RELAYS || 'ws://localhost:7777,wss://relay.damus.io').split(','), lookupRelays: (import.meta.env.VITE_NOSTR_LOOKUP_RELAYS || 'wss://purplepag.es').split(','), diff --git a/src/main.ts b/src/main.ts index 804fbae..aa133b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,22 +4,28 @@ import router from './router' import App from './App.vue' import './style.css' import { registerSW } from 'virtual:pwa-register' +import { initMockMode } from './utils/mock' -const app = createApp(App) +// Detect backend availability before the app renders. +// If the backend is unreachable, USE_MOCK is auto-flipped to true +// so every service/store/composable falls back to mock data. +initMockMode().then(() => { + const app = createApp(App) -app.use(createPinia()) -app.use(router) + app.use(createPinia()) + app.use(router) -app.mount('#app') + app.mount('#app') -// Register PWA service worker with auto-update -const updateSW = registerSW({ - immediate: true, - onNeedRefresh() { - // Auto-reload when new content is available - updateSW(true) - }, - onOfflineReady() { - console.log('App ready to work offline') - }, + // Register PWA service worker with auto-update + const updateSW = registerSW({ + immediate: true, + onNeedRefresh() { + // Auto-reload when new content is available + updateSW(true) + }, + onOfflineReady() { + console.log('App ready to work offline') + }, + }) }) diff --git a/src/router/guards.ts b/src/router/guards.ts index 71cb1f5..3403c57 100644 --- a/src/router/guards.ts +++ b/src/router/guards.ts @@ -1,9 +1,21 @@ import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' import { useAuthStore } from '../stores/auth' +import { accountManager } from '../lib/accounts' +import { USE_MOCK } from '../utils/mock' + +/** + * Check if user is authenticated through any method: + * - Auth store (Cognito/Nostr session) + * - Nostr account manager (persona/extension login) + */ +function isUserAuthenticated(): boolean { + const authStore = useAuthStore() + return authStore.isAuthenticated || !!accountManager.active +} /** * Authentication guard - * Redirects to login if not authenticated + * Redirects to home if not authenticated (auth modal can be opened there) */ export async function authGuard( to: RouteLocationNormalized, @@ -17,12 +29,12 @@ export async function authGuard( await authStore.initialize() } - if (authStore.isAuthenticated) { + if (isUserAuthenticated()) { next() } else { // Store intended destination for redirect after login sessionStorage.setItem('redirect_after_login', to.fullPath) - next('/login') + next('/') } } @@ -35,9 +47,7 @@ export function guestGuard( _from: RouteLocationNormalized, next: NavigationGuardNext ) { - const authStore = useAuthStore() - - if (authStore.isAuthenticated) { + if (isUserAuthenticated()) { next('/') } else { next() @@ -55,37 +65,55 @@ export function subscriptionGuard( ) { const authStore = useAuthStore() - if (!authStore.isAuthenticated) { + if (!isUserAuthenticated()) { sessionStorage.setItem('redirect_after_login', to.fullPath) - next('/login') + next('/') } else if (authStore.hasActiveSubscription()) { next() } else { - // Redirect to subscription page - next('/subscription') + next('/') } } /** * Filmmaker guard - * Restricts access to filmmaker-only routes + * Restricts access to filmmaker-only routes. + * In development mode with mock data, allows any authenticated user + * (mock Nostr logins include filmmaker profile). */ -export function filmmakerGuard( +export async function filmmakerGuard( to: RouteLocationNormalized, _from: RouteLocationNormalized, next: NavigationGuardNext ) { const authStore = useAuthStore() - if (!authStore.isAuthenticated) { - sessionStorage.setItem('redirect_after_login', to.fullPath) - next('/login') - } else if (authStore.isFilmmaker()) { - next() - } else { - // Redirect to home with error message - next('/') + // Initialize auth if not already done + if (!authStore.isAuthenticated && !authStore.isLoading) { + await authStore.initialize() } + + if (!isUserAuthenticated()) { + sessionStorage.setItem('redirect_after_login', to.fullPath) + next('/') + return + } + + // Auth store knows about filmmaker status + if (authStore.isFilmmaker()) { + next() + return + } + + // In dev/mock mode, Nostr account logins are treated as filmmakers + // since the mock login creates a filmmaker profile + if (USE_MOCK && accountManager.active) { + next() + return + } + + // Not a filmmaker — redirect to home + next('/') } /** diff --git a/src/router/index.ts b/src/router/index.ts index 0c3ceb5..107ea48 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,5 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' -import { setupGuards, authGuard } from './guards' +import { setupGuards, authGuard, filmmakerGuard } from './guards' import Browse from '../views/Browse.vue' const router = createRouter({ @@ -23,7 +23,37 @@ const router = createRouter({ component: () => import('../views/Profile.vue'), beforeEnter: authGuard, meta: { requiresAuth: true } - } + }, + + // ── Creator / Filmmaker Routes ──────────────────────────────────────────── + { + path: '/backstage', + name: 'backstage', + component: () => import('../views/backstage/Backstage.vue'), + beforeEnter: filmmakerGuard, + meta: { requiresAuth: true, requiresFilmmaker: true } + }, + { + path: '/backstage/project/:id', + name: 'project-editor', + component: () => import('../views/backstage/ProjectEditor.vue'), + beforeEnter: filmmakerGuard, + meta: { requiresAuth: true, requiresFilmmaker: true } + }, + { + path: '/backstage/analytics', + name: 'analytics', + component: () => import('../views/backstage/Analytics.vue'), + beforeEnter: filmmakerGuard, + meta: { requiresAuth: true, requiresFilmmaker: true } + }, + { + path: '/backstage/settings', + name: 'filmmaker-settings', + component: () => import('../views/backstage/Settings.vue'), + beforeEnter: filmmakerGuard, + meta: { requiresAuth: true, requiresFilmmaker: true } + }, ] }) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cf68207..4ad743d 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -55,9 +55,9 @@ class ApiService { return this.client(originalRequest) } } catch (refreshError) { - // Token refresh failed - clear auth and redirect to login + // Token refresh failed - clear auth and redirect to home this.clearAuth() - window.location.href = '/login' + window.location.href = '/' return Promise.reject(refreshError) } } @@ -100,6 +100,7 @@ class ApiService { public clearAuth() { sessionStorage.removeItem('auth_token') sessionStorage.removeItem('nostr_token') + sessionStorage.removeItem('nostr_pubkey') sessionStorage.removeItem('refresh_token') } @@ -119,13 +120,13 @@ class ApiService { throw new Error('No refresh token available') } - // Call refresh endpoint (implement based on backend) - const response = await axios.post(`${apiConfig.baseURL}/auth/refresh`, { + // Call Nostr refresh endpoint + const response = await axios.post(`${apiConfig.baseURL}/auth/nostr/refresh`, { refreshToken, }) const newToken = response.data.accessToken - this.setToken(newToken, 'cognito') + this.setToken(newToken, 'nostr') if (response.data.refreshToken) { sessionStorage.setItem('refresh_token', response.data.refreshToken) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 6e3b03e..ce83a05 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,4 +1,9 @@ +import axios from 'axios' import { apiService } from './api.service' +import { nip98Service } from './nip98.service' +import { apiConfig } from '../config/api.config' +import { accountManager } from '../lib/accounts' +import type { EventTemplate } from 'nostr-tools' import type { LoginCredentials, RegisterData, @@ -8,6 +13,48 @@ import type { ApiUser, } from '../types/api' +/** + * Create a NIP-98 HTTP Auth event (kind 27235) and return it as + * a base64-encoded Authorization header value: `Nostr ` + */ +async function createNip98AuthHeader( + url: string, + method: string, + body?: string, +): Promise { + const account = accountManager.active + if (!account) throw new Error('No active Nostr account') + + const tags: string[][] = [ + ['u', url], + ['method', method.toUpperCase()], + ] + + // If there's a body, include its SHA-256 hash + if (body && body.length > 0) { + const encoder = new TextEncoder() + const hash = await crypto.subtle.digest('SHA-256', encoder.encode(body)) + const hashHex = Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + tags.push(['payload', hashHex]) + } + + const template: EventTemplate = { + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + tags, + content: '', + } + + // Sign with the account's signer + const signed = await account.signer.signEvent(template) + + // Base64-encode the signed event JSON + const b64 = btoa(JSON.stringify(signed)) + return `Nostr ${b64}` +} + /** * Authentication Service * Handles Cognito and Nostr authentication @@ -68,17 +115,41 @@ class AuthService { } /** - * Create Nostr session + * Create Nostr session via NIP-98 HTTP Auth. + * Signs a kind-27235 event and sends it as the Authorization header. */ - async createNostrSession(request: NostrSessionRequest): Promise { - const response = await apiService.post('/auth/nostr/session', request) + async createNostrSession(_request: NostrSessionRequest): Promise { + const url = `${apiConfig.baseURL}/auth/nostr/session` + const method = 'POST' + + // Create NIP-98 auth header — no body is sent + const authHeader = await createNip98AuthHeader(url, method) + + // Send the request without a body. + // We use axios({ method }) instead of axios.post(url, data) to + // guarantee no Content-Type or body is serialized. + const response = await axios({ + method: 'POST', + url, + headers: { Authorization: authHeader }, + timeout: apiConfig.timeout, + }) - // Store Nostr token - if (response.token) { - apiService.setToken(response.token, 'nostr') + const data = response.data + + // Map accessToken to token for convenience + data.token = data.accessToken + + // Store Nostr JWT for subsequent authenticated requests + if (data.accessToken) { + apiService.setToken(data.accessToken, 'nostr') + sessionStorage.setItem('nostr_token', data.accessToken) + + // Also populate nip98Service storage so IndeehubApiService can find the token + nip98Service.storeTokens(data.accessToken, data.refreshToken, data.expiresIn) } - return response + return data } /** diff --git a/src/services/filmmaker.service.ts b/src/services/filmmaker.service.ts new file mode 100644 index 0000000..f516676 --- /dev/null +++ b/src/services/filmmaker.service.ts @@ -0,0 +1,382 @@ +/** + * Filmmaker Service + * + * Source-aware API client for all filmmaker/creator endpoints. + * Routes calls to either the original API (api.service) or + * our self-hosted backend (indeehub-api.service) based on the + * active content source. + */ + +import { apiService } from './api.service' +import { indeehubApiService } from './indeehub-api.service' +import { useContentSourceStore } from '../stores/contentSource' +import type { + ApiProject, + ApiContent, + ApiSeason, + ApiFilmmakerAnalytics, + ApiWatchAnalytics, + ApiPaymentMethod, + ApiPayment, + ApiGenre, + CreateProjectData, + UpdateProjectData, + UploadInitResponse, + UploadPresignedUrlsResponse, +} from '../types/api' + +/** + * Check if we should route requests to our self-hosted backend + */ +function usesSelfHosted(): boolean { + const store = useContentSourceStore() + return store.activeSource === 'indeehub-api' +} + +// ── Projects ──────────────────────────────────────────────────────────────────── + +export async function getPrivateProjects(filters?: { + type?: string + status?: string + search?: string + sort?: string + limit?: number + offset?: number +}): Promise { + if (usesSelfHosted()) { + const params = new URLSearchParams() + if (filters?.type) params.append('type', filters.type) + if (filters?.status) params.append('status', filters.status) + if (filters?.search) params.append('search', filters.search) + if (filters?.sort) params.append('sort', filters.sort) + if (filters?.limit) params.append('limit', String(filters.limit)) + if (filters?.offset) params.append('offset', String(filters.offset)) + const qs = params.toString() + return indeehubApiService.get(`/projects/private${qs ? `?${qs}` : ''}`) + } + return apiService.get('/projects/private', { params: filters }) +} + +export async function getPrivateProjectsCount(filters?: { + type?: string + status?: string + search?: string +}): Promise { + if (usesSelfHosted()) { + const params = new URLSearchParams() + if (filters?.type) params.append('type', filters.type) + if (filters?.status) params.append('status', filters.status) + if (filters?.search) params.append('search', filters.search) + const qs = params.toString() + const res = await indeehubApiService.get<{ count: number }>(`/projects/private/count${qs ? `?${qs}` : ''}`) + return res.count + } + const res = await apiService.get<{ count: number }>('/projects/private/count', { params: filters }) + return res.count +} + +export async function getPrivateProject(id: string): Promise { + if (usesSelfHosted()) { + return indeehubApiService.get(`/projects/private/${id}`) + } + return apiService.get(`/projects/private/${id}`) +} + +export async function createProject(data: CreateProjectData): Promise { + if (usesSelfHosted()) { + return indeehubApiService.post('/projects', data) + } + return apiService.post('/projects', data) +} + +export async function updateProject(id: string, data: UpdateProjectData): Promise { + if (usesSelfHosted()) { + return indeehubApiService.patch(`/projects/${id}`, data) + } + return apiService.patch(`/projects/${id}`, data) +} + +export async function deleteProject(id: string): Promise { + if (usesSelfHosted()) { + await indeehubApiService.delete(`/projects/${id}`) + return + } + await apiService.delete(`/projects/${id}`) +} + +export async function checkSlugExists(projectId: string, slug: string): Promise { + if (usesSelfHosted()) { + const res = await indeehubApiService.get<{ exists: boolean }>(`/projects/${projectId}/slug/${slug}/exists`) + return res.exists + } + const res = await apiService.get<{ exists: boolean }>(`/projects/${projectId}/slug/${slug}/exists`) + return res.exists +} + +// ── Content (episodes/seasons) ────────────────────────────────────────────────── + +export async function getContents(projectId: string): Promise { + if (usesSelfHosted()) { + return indeehubApiService.get(`/contents/project/${projectId}`) + } + return apiService.get(`/contents/project/${projectId}`) +} + +export async function upsertContent( + projectId: string, + data: Partial +): Promise { + if (usesSelfHosted()) { + return indeehubApiService.patch(`/contents/project/${projectId}`, data) + } + return apiService.patch(`/contents/project/${projectId}`, data) +} + +export async function getSeasons(projectId: string): Promise { + if (usesSelfHosted()) { + return indeehubApiService.get(`/seasons/project/${projectId}`) + } + return apiService.get(`/seasons/project/${projectId}`) +} + +// ── Upload ────────────────────────────────────────────────────────────────────── + +export async function initializeUpload( + key: string, + bucket: string, + contentType: string +): Promise { + if (usesSelfHosted()) { + return indeehubApiService.post('/upload/initialize', { + Key: key, + Bucket: bucket, + ContentType: contentType, + }) + } + return apiService.post('/upload/initialize', { + Key: key, + Bucket: bucket, + ContentType: contentType, + }) +} + +export async function getPresignedUrls( + uploadId: string, + key: string, + bucket: string, + parts: number +): Promise { + if (usesSelfHosted()) { + return indeehubApiService.post('/upload/presigned-urls', { + UploadId: uploadId, + Key: key, + Bucket: bucket, + parts, + }) + } + return apiService.post('/upload/presigned-urls', { + UploadId: uploadId, + Key: key, + Bucket: bucket, + parts, + }) +} + +export async function finalizeUpload( + uploadId: string, + key: string, + bucket: string, + parts: Array<{ PartNumber: number; ETag: string }> +): Promise { + const payload = { UploadId: uploadId, Key: key, Bucket: bucket, parts } + if (usesSelfHosted()) { + await indeehubApiService.post('/upload/finalize', payload) + return + } + await apiService.post('/upload/finalize', payload) +} + +// ── Analytics ─────────────────────────────────────────────────────────────────── + +export async function getFilmmakerAnalytics(): Promise { + if (usesSelfHosted()) { + return indeehubApiService.get('/filmmakers/analytics') + } + return apiService.get('/filmmakers/analytics') +} + +export async function getWatchAnalytics( + projectIds?: string[], + dateRange?: { start: string; end: string } +): Promise { + const params: Record = {} + if (projectIds?.length) params['projectIds'] = projectIds.join(',') + if (dateRange) { + params['start'] = dateRange.start + params['end'] = dateRange.end + } + + if (usesSelfHosted()) { + const qs = new URLSearchParams(params).toString() + return indeehubApiService.get(`/filmmakers/watch-analytics${qs ? `?${qs}` : ''}`) + } + return apiService.get('/filmmakers/watch-analytics', { params }) +} + +export async function getProjectRevenue( + projectIds: string[], + dateRange?: { start: string; end: string } +): Promise> { + const params: Record = { ids: projectIds.join(',') } + if (dateRange) { + params['start'] = dateRange.start + params['end'] = dateRange.end + } + + if (usesSelfHosted()) { + const qs = new URLSearchParams(params).toString() + return indeehubApiService.get>(`/projects/revenue${qs ? `?${qs}` : ''}`) + } + return apiService.get>('/projects/revenue', { params }) +} + +// ── Payments / Withdrawal ─────────────────────────────────────────────────────── + +export async function getPaymentMethods(): Promise { + if (usesSelfHosted()) { + return indeehubApiService.get('/payment-methods') + } + return apiService.get('/payment-methods') +} + +export async function addPaymentMethod(data: { + type: 'lightning' | 'bank' + lightningAddress?: string + bankName?: string + accountNumber?: string + routingNumber?: string + withdrawalFrequency: 'manual' | 'weekly' | 'monthly' +}): Promise { + if (usesSelfHosted()) { + return indeehubApiService.post('/payment-methods', data) + } + return apiService.post('/payment-methods', data) +} + +export async function updatePaymentMethod( + id: string, + data: Partial +): Promise { + if (usesSelfHosted()) { + return indeehubApiService.patch(`/payment-methods/${id}`, data) + } + return apiService.patch(`/payment-methods/${id}`, data) +} + +export async function selectPaymentMethod(id: string): Promise { + if (usesSelfHosted()) { + await indeehubApiService.patch(`/payment-methods/${id}/select`, {}) + return + } + await apiService.patch(`/payment-methods/${id}/select`, {}) +} + +export async function removePaymentMethod(id: string): Promise { + if (usesSelfHosted()) { + await indeehubApiService.delete(`/payment-methods/${id}`) + return + } + await apiService.delete(`/payment-methods/${id}`) +} + +export async function validateLightningAddress(address: string): Promise { + if (usesSelfHosted()) { + const res = await indeehubApiService.post<{ valid: boolean }>('/payment/validate-lightning-address', { address }) + return res.valid + } + const res = await apiService.post<{ valid: boolean }>('/payment/validate-lightning-address', { address }) + return res.valid +} + +export async function getPayments(filmId?: string): Promise { + const params = filmId ? { filmId } : undefined + if (usesSelfHosted()) { + const qs = filmId ? `?filmId=${filmId}` : '' + return indeehubApiService.get(`/payment${qs}`) + } + return apiService.get('/payment', { params }) +} + +export async function withdraw(amount: number, filmId?: string): Promise { + const payload: Record = { amount } + if (filmId) payload.filmId = filmId + if (usesSelfHosted()) { + return indeehubApiService.post('/payment/bank-payout', payload) + } + return apiService.post('/payment/bank-payout', payload) +} + +// ── Genres ─────────────────────────────────────────────────────────────────────── + +export async function getGenres(): Promise { + if (usesSelfHosted()) { + return indeehubApiService.get('/genres') + } + return apiService.get('/genres') +} + +// ── SAT Price ─────────────────────────────────────────────────────────────────── + +export async function getSatPrice(): Promise { + if (usesSelfHosted()) { + const res = await indeehubApiService.get<{ price: number }>('/payment/sat-price') + return res.price + } + const res = await apiService.get<{ price: number }>('/payment/sat-price') + return res.price +} + +/** + * Grouped export for convenience + */ +export const filmmakerService = { + // Projects + getPrivateProjects, + getPrivateProjectsCount, + getPrivateProject, + createProject, + updateProject, + deleteProject, + checkSlugExists, + + // Content + getContents, + upsertContent, + getSeasons, + + // Upload + initializeUpload, + getPresignedUrls, + finalizeUpload, + + // Analytics + getFilmmakerAnalytics, + getWatchAnalytics, + getProjectRevenue, + + // Payments + getPaymentMethods, + addPaymentMethod, + updatePaymentMethod, + selectPaymentMethod, + removePaymentMethod, + validateLightningAddress, + getPayments, + withdraw, + + // Genres + getGenres, + + // SAT Price + getSatPrice, +} diff --git a/src/services/indeehub-api.service.ts b/src/services/indeehub-api.service.ts new file mode 100644 index 0000000..83279bb --- /dev/null +++ b/src/services/indeehub-api.service.ts @@ -0,0 +1,190 @@ +/** + * IndeeHub Self-Hosted API Service + * + * Dedicated API client for our self-hosted NestJS backend. + * Uses the /api/ proxy configured in nginx. + * Auth tokens are managed by nip98.service.ts. + */ + +import axios, { type AxiosInstance } from 'axios' +import { indeehubApiConfig } from '../config/api.config' +import { nip98Service } from './nip98.service' + +class IndeehubApiService { + private client: AxiosInstance + + constructor() { + this.client = axios.create({ + baseURL: indeehubApiConfig.baseURL, + timeout: indeehubApiConfig.timeout, + headers: { + 'Content-Type': 'application/json', + }, + }) + + // Attach JWT token from NIP-98 session + this.client.interceptors.request.use((config) => { + const token = nip98Service.accessToken + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }) + + // Auto-refresh on 401 + this.client.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + const newToken = await nip98Service.refresh() + if (newToken) { + originalRequest.headers.Authorization = `Bearer ${newToken}` + return this.client(originalRequest) + } + } + return Promise.reject(error) + } + ) + } + + /** + * Generic typed GET request + */ + async get(url: string): Promise { + const response = await this.client.get(url) + return response.data + } + + /** + * Generic typed POST request + */ + async post(url: string, data?: any): Promise { + const response = await this.client.post(url, data) + return response.data + } + + /** + * Generic typed PATCH request + */ + async patch(url: string, data?: any): Promise { + const response = await this.client.patch(url, data) + return response.data + } + + /** + * Generic typed DELETE request + */ + async delete(url: string): Promise { + const response = await this.client.delete(url) + return response.data + } + + /** + * Check if the API is reachable + */ + async healthCheck(): Promise { + try { + await this.client.get('/nostr-auth/health', { timeout: 5000 }) + return true + } catch { + return false + } + } + + /** + * Get all published projects + */ + async getProjects(filters?: { + status?: string + type?: string + genre?: string + limit?: number + offset?: number + }): Promise { + const params = new URLSearchParams() + if (filters?.status) params.append('status', filters.status) + if (filters?.type) params.append('type', filters.type) + if (filters?.genre) params.append('genre', filters.genre) + if (filters?.limit) params.append('limit', String(filters.limit)) + if (filters?.offset) params.append('offset', String(filters.offset)) + + const url = `/projects${params.toString() ? `?${params.toString()}` : ''}` + const response = await this.client.get(url) + return response.data + } + + /** + * Get a single project by ID + */ + async getProject(id: string): Promise { + const response = await this.client.get(`/projects/${id}`) + return response.data + } + + /** + * Get streaming URL for a content item + * Returns different data based on deliveryMode (native vs partner) + */ + async getStreamingUrl(contentId: string): Promise<{ + url: string + deliveryMode: 'native' | 'partner' + keyUrl?: string + drmToken?: string + }> { + const response = await this.client.get(`/contents/${contentId}/stream`) + return response.data + } + + /** + * Get user's library items + */ + async getLibrary(): Promise { + const response = await this.client.get('/library') + return response.data + } + + /** + * Add a project to user's library + */ + async addToLibrary(projectId: string): Promise { + const response = await this.client.post('/library', { projectId }) + return response.data + } + + /** + * Remove a project from user's library + */ + async removeFromLibrary(projectId: string): Promise { + await this.client.delete(`/library/${projectId}`) + } + + /** + * Get current user profile (requires auth) + */ + async getMe(): Promise { + const response = await this.client.get('/auth/me') + return response.data + } + + /** + * Get genres + */ + async getGenres(): Promise { + const response = await this.client.get('/genres') + return response.data + } + + /** + * Get the CDN URL for a storage path + */ + getCdnUrl(path: string): string { + if (!path) return '' + if (path.startsWith('http')) return path + if (path.startsWith('/')) return path + return `${indeehubApiConfig.cdnURL}/${path}` + } +} + +export const indeehubApiService = new IndeehubApiService() diff --git a/src/services/library.service.ts b/src/services/library.service.ts index f14b108..2840445 100644 --- a/src/services/library.service.ts +++ b/src/services/library.service.ts @@ -1,5 +1,6 @@ import { apiService } from './api.service' import type { ApiRent, ApiContent } from '../types/api' +import { USE_MOCK } from '../utils/mock' /** * Library Service @@ -14,10 +15,7 @@ class LibraryService { rented: ApiRent[] continueWatching: Array<{ content: ApiContent; progress: number }> }> { - // Check if we're in development mode - const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV - - if (useMockData) { + if (USE_MOCK) { // Mock library data for development console.log('🔧 Development mode: Using mock library data') @@ -81,15 +79,31 @@ class LibraryService { } /** - * Rent content + * Rent content via Lightning invoice. + * Calls POST /rents/lightning which creates a BTCPay invoice. + * Returns the invoice details including BOLT11 for QR code display. */ - async rentContent(contentId: string, paymentMethodId?: string): Promise { - return apiService.post('/rents', { - contentId, - paymentMethodId, + async rentContent(contentId: string, couponCode?: string): Promise<{ + id: string + contentId: string + lnInvoice: string + expiration: string + sourceAmount: { amount: string; currency: string } + conversionRate: { amount: string; sourceCurrency: string; targetCurrency: string } + }> { + return apiService.post('/rents/lightning', { + id: contentId, + couponCode, }) } + /** + * Check if a rent exists for a given content ID (for polling after payment). + */ + async checkRentExists(contentId: string): Promise<{ exists: boolean }> { + return apiService.get(`/rents/content/${contentId}/exists`) + } + /** * Check if user has access to content */ diff --git a/src/services/nip98.service.ts b/src/services/nip98.service.ts new file mode 100644 index 0000000..46fb06d --- /dev/null +++ b/src/services/nip98.service.ts @@ -0,0 +1,172 @@ +/** + * NIP-98 Authentication Bridge + * + * Handles Nostr-based authentication with the self-hosted backend: + * 1. Signs NIP-98 HTTP Auth events (kind 27235) + * 2. Exchanges them for JWT session tokens + * 3. Manages token storage and auto-refresh + */ + +import axios from 'axios' +import { indeehubApiConfig } from '../config/api.config' + +const TOKEN_KEY = 'indeehub_api_token' +const REFRESH_KEY = 'indeehub_api_refresh' +const EXPIRES_KEY = 'indeehub_api_expires' + +class Nip98Service { + private refreshPromise: Promise | null = null + + /** + * Check if we have a valid (non-expired) API token + */ + get hasValidToken(): boolean { + const token = sessionStorage.getItem(TOKEN_KEY) + const expires = sessionStorage.getItem(EXPIRES_KEY) + if (!token || !expires) return false + return Date.now() < Number(expires) + } + + /** + * Get the current access token + */ + get accessToken(): string | null { + if (!this.hasValidToken) return null + return sessionStorage.getItem(TOKEN_KEY) + } + + /** + * Create a session with the backend using a NIP-98 auth event. + * + * The signer creates a kind 27235 event targeting POST /auth/nostr/session. + * The backend verifies the event and returns JWT tokens. + * + * @param signer - An applesauce signer instance (or any object with sign() method) + * @param pubkey - The Nostr pubkey (hex) + */ + async createSession(signer: any, pubkey: string): Promise { + try { + const url = `${indeehubApiConfig.baseURL}/auth/nostr/session` + const now = Math.floor(Date.now() / 1000) + + // Build the NIP-98 event + const event = { + kind: 27235, + created_at: now, + tags: [ + ['u', url], + ['method', 'POST'], + ], + content: '', + pubkey, + } + + // Sign the event using the Nostr signer + let signedEvent: any + if (typeof signer.sign === 'function') { + signedEvent = await signer.sign(event) + } else if (typeof signer.signEvent === 'function') { + signedEvent = await signer.signEvent(event) + } else { + throw new Error('Signer does not have a sign or signEvent method') + } + + // Base64-encode the signed event + const encodedEvent = btoa(JSON.stringify(signedEvent)) + + // Send to backend — no body to avoid NIP-98 payload mismatch + const response = await axios({ + method: 'POST', + url, + headers: { + Authorization: `Nostr ${encodedEvent}`, + }, + timeout: 15000, + }) + + const { accessToken, refreshToken, expiresIn } = response.data + + // Store tokens + sessionStorage.setItem(TOKEN_KEY, accessToken) + sessionStorage.setItem(REFRESH_KEY, refreshToken) + sessionStorage.setItem( + EXPIRES_KEY, + String(Date.now() + (expiresIn * 1000) - 30000) // 30s buffer + ) + + // Also set on the main apiService for backwards compatibility + sessionStorage.setItem('nostr_token', accessToken) + + return true + } catch (error) { + console.error('[nip98] Failed to create session:', error) + return false + } + } + + /** + * Refresh the session using the stored refresh token + */ + async refresh(): Promise { + if (this.refreshPromise) return this.refreshPromise + + this.refreshPromise = (async () => { + try { + const refreshToken = sessionStorage.getItem(REFRESH_KEY) + if (!refreshToken) return null + + const response = await axios.post( + `${indeehubApiConfig.baseURL}/auth/nostr/refresh`, + { refreshToken }, + { timeout: 15000 } + ) + + const { accessToken, refreshToken: newRefresh, expiresIn } = response.data + + sessionStorage.setItem(TOKEN_KEY, accessToken) + if (newRefresh) sessionStorage.setItem(REFRESH_KEY, newRefresh) + sessionStorage.setItem( + EXPIRES_KEY, + String(Date.now() + (expiresIn * 1000) - 30000) + ) + sessionStorage.setItem('nostr_token', accessToken) + + return accessToken + } catch { + this.clearSession() + return null + } finally { + this.refreshPromise = null + } + })() + + return this.refreshPromise + } + + /** + * Store tokens from an external auth flow (e.g. auth.service.ts). + * Keeps nip98Service in sync so IndeehubApiService can read the token. + */ + storeTokens(accessToken: string, refreshToken?: string, expiresIn?: number) { + sessionStorage.setItem(TOKEN_KEY, accessToken) + if (refreshToken) { + sessionStorage.setItem(REFRESH_KEY, refreshToken) + } + // Default to 1 hour if expiresIn is not provided + const ttlMs = (expiresIn ?? 3600) * 1000 + sessionStorage.setItem(EXPIRES_KEY, String(Date.now() + ttlMs - 30000)) + // Backwards compatibility + sessionStorage.setItem('nostr_token', accessToken) + } + + /** + * Clear stored session data + */ + clearSession() { + sessionStorage.removeItem(TOKEN_KEY) + sessionStorage.removeItem(REFRESH_KEY) + sessionStorage.removeItem(EXPIRES_KEY) + } +} + +export const nip98Service = new Nip98Service() diff --git a/src/services/subscription.service.ts b/src/services/subscription.service.ts index 6610df9..0610c8d 100644 --- a/src/services/subscription.service.ts +++ b/src/services/subscription.service.ts @@ -46,6 +46,22 @@ class SubscriptionService { return apiService.post(`/subscriptions/${subscriptionId}/resume`) } + /** + * Create a Lightning subscription invoice via BTCPay. + * Returns invoice details including BOLT11 for QR code display. + */ + async createLightningSubscription(data: { + type: 'enthusiast' | 'film-buff' | 'cinephile' + period: 'monthly' | 'annual' + }): Promise<{ + lnInvoice: string + expiration: string + sourceAmount: { amount: string; currency: string } + id: string + }> { + return apiService.post('/subscriptions/lightning', data) + } + /** * Update payment method */ diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 750428d..ced26cf 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,7 +1,73 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { authService } from '../services/auth.service' +import { nip98Service } from '../services/nip98.service' import type { ApiUser } from '../types/api' +import { USE_MOCK } from '../utils/mock' + +/** Returns true when the error looks like a network / connection failure */ +function isConnectionError(error: any): boolean { + const msg = error?.message?.toLowerCase() || '' + return ( + msg.includes('unable to connect') || + msg.includes('network error') || + msg.includes('failed to fetch') || + msg.includes('econnrefused') + ) +} + +/** Build a mock Nostr user with filmmaker profile + subscription */ +function buildMockNostrUser(pubkey: string) { + const mockUserId = 'mock-nostr-user-' + pubkey.slice(0, 8) + return { + id: mockUserId, + email: `${pubkey.slice(0, 8)}@nostr.local`, + legalName: 'Nostr User', + nostrPubkey: pubkey, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + filmmaker: { + id: 'mock-filmmaker-' + pubkey.slice(0, 8), + userId: mockUserId, + professionalName: 'Nostr Filmmaker', + bio: 'Independent filmmaker and content creator.', + }, + subscriptions: [{ + id: 'mock-sub-cinephile', + userId: mockUserId, + tier: 'cinephile' as const, + status: 'active' as const, + currentPeriodStart: new Date().toISOString(), + currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + cancelAtPeriodEnd: false, + stripePriceId: 'mock-price-cinephile', + stripeCustomerId: 'mock-customer-' + pubkey.slice(0, 8), + }], + } +} + +/** Build a mock Cognito user with subscription */ +function buildMockCognitoUser(email: string, legalName?: string) { + const username = email.split('@')[0] + return { + id: 'mock-user-' + username, + email, + legalName: legalName || username.charAt(0).toUpperCase() + username.slice(1), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + subscriptions: [{ + id: 'mock-sub-cinephile', + userId: 'mock-user-' + username, + tier: 'cinephile' as const, + status: 'active' as const, + currentPeriodStart: new Date().toISOString(), + currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + cancelAtPeriodEnd: false, + stripePriceId: 'mock-price-cinephile', + stripeCustomerId: 'mock-customer-' + username, + }], + } +} export type AuthType = 'cognito' | 'nostr' | null @@ -28,35 +94,73 @@ export const useAuthStore = defineStore('auth', () => { const isLoading = ref(false) /** - * Initialize auth state from stored tokens + * Initialize auth state from stored tokens. + * In dev/mock mode, reconstructs the mock user directly from + * sessionStorage rather than calling the (non-existent) backend API. */ async function initialize() { + // Bail out early if already authenticated — prevents + // re-initialization from wiping state on subsequent navigations + if (isAuthenticated.value && user.value) { + return + } + isLoading.value = true try { - // Check for existing tokens const storedCognitoToken = sessionStorage.getItem('auth_token') const storedNostrToken = sessionStorage.getItem('nostr_token') + const storedPubkey = sessionStorage.getItem('nostr_pubkey') - if (storedCognitoToken || storedNostrToken) { - // Validate session and fetch user + if (!storedCognitoToken && !storedNostrToken) { + return // Nothing stored — not logged in + } + + // Helper: restore mock session from sessionStorage + const restoreAsMock = () => { + if (storedNostrToken && storedPubkey) { + user.value = buildMockNostrUser(storedPubkey) + nostrPubkey.value = storedPubkey + authType.value = 'nostr' + isAuthenticated.value = true + } else if (storedCognitoToken) { + user.value = buildMockCognitoUser('dev@local', 'Dev User') + cognitoToken.value = storedCognitoToken + authType.value = 'cognito' + isAuthenticated.value = true + } + } + + if (USE_MOCK) { + restoreAsMock() + return + } + + // Real mode: validate session with backend API + try { const isValid = await authService.validateSession() - + if (isValid) { await fetchCurrentUser() - + if (storedCognitoToken) { authType.value = 'cognito' cognitoToken.value = storedCognitoToken } else { authType.value = 'nostr' } - + isAuthenticated.value = true } else { - // Session invalid - clear auth await logout() } + } catch (apiError: any) { + if (isConnectionError(apiError)) { + console.warn('Backend not reachable — falling back to mock session.') + restoreAsMock() + } else { + throw apiError + } } } catch (error) { console.error('Failed to initialize auth:', error) @@ -71,74 +175,49 @@ export const useAuthStore = defineStore('auth', () => { */ async function loginWithCognito(email: string, password: string) { isLoading.value = true - - try { - // Check if we're in development mode without backend - const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV - - if (useMockData) { - // Mock Cognito login for development - console.log('🔧 Development mode: Using mock Cognito authentication') - - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 500)) - - // Create a mock user with active subscription - const mockUser = { - id: 'mock-user-' + email.split('@')[0], - email: email, - legalName: email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - subscriptions: [{ - id: 'mock-sub-cinephile', - userId: 'mock-user-' + email.split('@')[0], - tier: 'cinephile' as const, - status: 'active' as const, - currentPeriodStart: new Date().toISOString(), - currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now - cancelAtPeriodEnd: false, - stripePriceId: 'mock-price-cinephile', - stripeCustomerId: 'mock-customer-' + email.split('@')[0], - }], - } - - console.log('✅ Mock user created with Cinephile subscription (full access)') - - cognitoToken.value = 'mock-jwt-token-' + Date.now() - authType.value = 'cognito' - user.value = mockUser - isAuthenticated.value = true - - // Store mock tokens - sessionStorage.setItem('auth_token', cognitoToken.value) - sessionStorage.setItem('refresh_token', 'mock-refresh-token') - - return { - accessToken: cognitoToken.value, - idToken: 'mock-id-token', - refreshToken: 'mock-refresh-token', - expiresIn: 3600, - } + + // Mock Cognito login helper + const mockLogin = () => { + const mockUser = buildMockCognitoUser(email) + console.log('🔧 Using mock Cognito authentication') + + cognitoToken.value = 'mock-jwt-token-' + Date.now() + authType.value = 'cognito' + user.value = mockUser + isAuthenticated.value = true + + sessionStorage.setItem('auth_token', cognitoToken.value) + sessionStorage.setItem('refresh_token', 'mock-refresh-token') + + return { + accessToken: cognitoToken.value, + idToken: 'mock-id-token', + refreshToken: 'mock-refresh-token', + expiresIn: 3600, } - + } + + try { + if (USE_MOCK) { + await new Promise(resolve => setTimeout(resolve, 500)) + return mockLogin() + } + // Real API call const response = await authService.login({ email, password }) - + cognitoToken.value = response.accessToken authType.value = 'cognito' - + await fetchCurrentUser() - + isAuthenticated.value = true - + return response } catch (error: any) { - // Provide helpful error message - if (error.message?.includes('Unable to connect')) { - throw new Error( - 'Backend API not available. To use real authentication, start the backend server and set VITE_USE_MOCK_DATA=false in .env' - ) + if (isConnectionError(error)) { + console.warn('Backend not reachable — falling back to mock Cognito login.') + return mockLogin() } throw error } finally { @@ -151,75 +230,56 @@ export const useAuthStore = defineStore('auth', () => { */ async function loginWithNostr(pubkey: string, signature: string, event: any) { isLoading.value = true - + + // Mock Nostr login helper + const mockLogin = () => { + const mockUser = buildMockNostrUser(pubkey) + console.warn('🔧 Using mock Nostr authentication (backend not available)') + + nostrPubkey.value = pubkey + authType.value = 'nostr' + user.value = mockUser + isAuthenticated.value = true + + sessionStorage.setItem('nostr_token', 'mock-nostr-token-' + pubkey.slice(0, 16)) + sessionStorage.setItem('nostr_pubkey', pubkey) + + return { token: 'mock-nostr-token', user: mockUser } + } + try { - // Check if we're in development mode without backend - const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV - - if (useMockData) { - // Mock Nostr login for development - console.log('🔧 Development mode: Using mock Nostr authentication') - - // Simulate API delay + if (USE_MOCK) { await new Promise(resolve => setTimeout(resolve, 500)) - - // Create a mock Nostr user with active subscription - const mockUser = { - id: 'mock-nostr-user-' + pubkey.slice(0, 8), - email: `${pubkey.slice(0, 8)}@nostr.local`, - legalName: 'Nostr User', - nostrPubkey: pubkey, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - subscriptions: [{ - id: 'mock-sub-cinephile', - userId: 'mock-nostr-user-' + pubkey.slice(0, 8), - tier: 'cinephile' as const, - status: 'active' as const, - currentPeriodStart: new Date().toISOString(), - currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now - cancelAtPeriodEnd: false, - stripePriceId: 'mock-price-cinephile', - stripeCustomerId: 'mock-customer-' + pubkey.slice(0, 8), - }], - } - - console.log('✅ Mock Nostr user created with Cinephile subscription (full access)') - console.log('📝 Nostr Pubkey:', pubkey) - - nostrPubkey.value = pubkey - authType.value = 'nostr' - user.value = mockUser - isAuthenticated.value = true - - // Store mock session - sessionStorage.setItem('nostr_token', 'mock-nostr-token-' + pubkey.slice(0, 16)) - - return { - token: 'mock-nostr-token', - user: mockUser, - } + return mockLogin() } - - // Real API call + + // Real API call — creates NIP-98 signed session via the active + // Nostr account in accountManager (set by useAccounts before this call) const response = await authService.createNostrSession({ pubkey, signature, event, }) - + nostrPubkey.value = pubkey authType.value = 'nostr' - user.value = response.user + sessionStorage.setItem('nostr_pubkey', pubkey) isAuthenticated.value = true - + + // Backend returns JWT tokens but no user object. + // Fetch the user profile with the new access token. + try { + await fetchCurrentUser() + } catch { + // User may not exist in DB yet — create a minimal local representation + user.value = buildMockNostrUser(pubkey) + } + return response } catch (error: any) { - // Provide helpful error message - if (error.message?.includes('Unable to connect')) { - throw new Error( - 'Backend API not available. To use real Nostr authentication, start the backend server and set VITE_USE_MOCK_DATA=false in .env' - ) + if (isConnectionError(error)) { + console.warn('Backend not reachable — falling back to mock Nostr login.') + return mockLogin() } throw error } finally { @@ -232,78 +292,53 @@ export const useAuthStore = defineStore('auth', () => { */ async function register(email: string, password: string, legalName: string) { isLoading.value = true - - try { - // Check if we're in development mode without backend - const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV - - if (useMockData) { - // Mock registration for development - console.log('🔧 Development mode: Using mock registration') - - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 500)) - - // Create a mock user with active subscription - const mockUser = { - id: 'mock-user-' + email.split('@')[0], - email: email, - legalName: legalName, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - subscriptions: [{ - id: 'mock-sub-cinephile', - userId: 'mock-user-' + email.split('@')[0], - tier: 'cinephile' as const, - status: 'active' as const, - currentPeriodStart: new Date().toISOString(), - currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now - cancelAtPeriodEnd: false, - stripePriceId: 'mock-price-cinephile', - stripeCustomerId: 'mock-customer-' + email.split('@')[0], - }], - } - - console.log('✅ Mock user registered with Cinephile subscription (full access)') - - cognitoToken.value = 'mock-jwt-token-' + Date.now() - authType.value = 'cognito' - user.value = mockUser - isAuthenticated.value = true - - // Store mock tokens - sessionStorage.setItem('auth_token', cognitoToken.value) - sessionStorage.setItem('refresh_token', 'mock-refresh-token') - - return { - accessToken: cognitoToken.value, - idToken: 'mock-id-token', - refreshToken: 'mock-refresh-token', - expiresIn: 3600, - } + + // Mock registration helper + const mockRegister = () => { + const mockUser = buildMockCognitoUser(email, legalName) + console.warn('🔧 Using mock registration (backend not available)') + + cognitoToken.value = 'mock-jwt-token-' + Date.now() + authType.value = 'cognito' + user.value = mockUser + isAuthenticated.value = true + + sessionStorage.setItem('auth_token', cognitoToken.value) + sessionStorage.setItem('refresh_token', 'mock-refresh-token') + + return { + accessToken: cognitoToken.value, + idToken: 'mock-id-token', + refreshToken: 'mock-refresh-token', + expiresIn: 3600, } - + } + + try { + if (USE_MOCK) { + await new Promise(resolve => setTimeout(resolve, 500)) + return mockRegister() + } + // Real API call const response = await authService.register({ email, password, legalName, }) - + cognitoToken.value = response.accessToken authType.value = 'cognito' - + await fetchCurrentUser() - + isAuthenticated.value = true - + return response } catch (error: any) { - // Provide helpful error message - if (error.message?.includes('Unable to connect')) { - throw new Error( - 'Backend API not available. To use real authentication, start the backend server and set VITE_USE_MOCK_DATA=false in .env' - ) + if (isConnectionError(error)) { + console.warn('Backend not reachable — falling back to mock registration.') + return mockRegister() } throw error } finally { @@ -335,6 +370,7 @@ export const useAuthStore = defineStore('auth', () => { */ async function logout() { await authService.logout() + nip98Service.clearSession() user.value = null authType.value = null diff --git a/src/stores/content.ts b/src/stores/content.ts index 142b186..9f55ae3 100644 --- a/src/stores/content.ts +++ b/src/stores/content.ts @@ -6,8 +6,9 @@ import { topDocFilms, topDocBitcoin, topDocCrypto, topDocMoney, topDocEconomics import { contentService } from '../services/content.service' import { mapApiProjectsToContents } from '../utils/mappers' import { useContentSourceStore } from './contentSource' - -const USE_MOCK_DATA = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV +import { indeehubApiService } from '../services/indeehub-api.service' +import { useFilmmaker } from '../composables/useFilmmaker' +import { USE_MOCK as USE_MOCK_DATA } from '../utils/mock' export const useContentStore = defineStore('content', () => { const featuredContent = ref(null) @@ -24,25 +25,21 @@ export const useContentStore = defineStore('content', () => { const error = ref(null) /** - * Fetch content from API + * Fetch content from the original API (external IndeeHub) */ async function fetchContentFromApi() { try { - // Fetch all published projects const projects = await contentService.getProjects({ status: 'published' }) if (projects.length === 0) { throw new Error('No content available') } - // Map API data to content format const allContent = mapApiProjectsToContents(projects) - // Set featured content (first film project or first project) const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0] featuredContent.value = featuredFilm - // Organize into rows const films = allContent.filter(c => c.type === 'film') const bitcoinContent = allContent.filter(c => c.categories?.some(cat => cat.toLowerCase().includes('bitcoin')) @@ -68,6 +65,64 @@ export const useContentStore = defineStore('content', () => { } } + /** + * Fetch content from our self-hosted IndeeHub API + */ + async function fetchContentFromIndeehubApi() { + try { + const response = await indeehubApiService.getProjects() + + // Handle both array responses and wrapped responses like { data: [...] } + const projects = Array.isArray(response) ? response : (response as any)?.data ?? [] + + if (!Array.isArray(projects) || projects.length === 0) { + throw new Error('No content available from IndeeHub API') + } + + // Map API projects to frontend Content format + const allContent: Content[] = projects.map((p: any) => ({ + id: p.id, + title: p.title, + description: p.synopsis || '', + thumbnail: p.poster || '', + backdrop: p.poster || '', + type: p.type || 'film', + slug: p.slug, + rentalPrice: p.rentalPrice, + status: p.status, + categories: p.genre ? [p.genre.name] : [], + streamingUrl: p.streamingUrl || p.partnerStreamUrl || undefined, + apiData: { + deliveryMode: p.deliveryMode, + partnerStreamUrl: p.partnerStreamUrl, + partnerDashUrl: p.partnerDashUrl, + partnerFairplayUrl: p.partnerFairplayUrl, + partnerDrmToken: p.partnerDrmToken, + }, + })) + + const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0] + featuredContent.value = featuredFilm + + const films = allContent.filter(c => c.type === 'film') + const bitcoinContent = allContent.filter(c => + c.categories?.some(cat => cat.toLowerCase().includes('bitcoin') || cat.toLowerCase().includes('documentary')) + ) + + contentRows.value = { + featured: allContent.slice(0, 10), + newReleases: films.slice(0, 8), + bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6), + documentaries: allContent.slice(0, 10), + dramas: films.slice(0, 6), + independent: films.slice(0, 10) + } + } catch (err) { + console.error('IndeeHub API fetch failed:', err) + throw err + } + } + /** * Fetch IndeeHub mock content (original catalog) */ @@ -111,7 +166,52 @@ export const useContentStore = defineStore('content', () => { } /** - * Route to the correct mock loader based on the active content source + * Convert published filmmaker projects to Content format and merge + * them into the existing content rows so they appear on the browse page. + */ + function mergePublishedFilmmakerProjects() { + try { + const { projects } = useFilmmaker() + const published = projects.value.filter(p => p.status === 'published') + if (published.length === 0) return + + const publishedContent: Content[] = published.map(p => ({ + id: p.id, + title: p.title || p.name, + description: p.synopsis || '', + thumbnail: p.poster || '/images/placeholder-poster.jpg', + backdrop: p.poster || '/images/placeholder-poster.jpg', + type: p.type === 'episodic' ? 'series' as const : 'film' as const, + rating: p.format || undefined, + releaseYear: p.releaseDate ? new Date(p.releaseDate).getFullYear() : new Date().getFullYear(), + categories: p.genres?.map(g => g.name) || [], + slug: p.slug, + rentalPrice: p.rentalPrice, + status: p.status, + apiData: p, + })) + + // Merge into each content row (prepend so they appear first) + for (const key of Object.keys(contentRows.value)) { + // Avoid duplicates by filtering out any already-present IDs + const existingIds = new Set(contentRows.value[key].map(c => c.id)) + const newItems = publishedContent.filter(c => !existingIds.has(c.id)) + if (newItems.length > 0) { + contentRows.value[key] = [...newItems, ...contentRows.value[key]] + } + } + + // If no featured content yet, use the first published project + if (!featuredContent.value && publishedContent.length > 0) { + featuredContent.value = publishedContent[0] + } + } catch { + // Filmmaker composable may not be initialized yet — safe to ignore + } + } + + /** + * Route to the correct loader based on the active content source */ function fetchContentFromMock() { const sourceStore = useContentSourceStore() @@ -120,6 +220,9 @@ export const useContentStore = defineStore('content', () => { } else { fetchIndeeHubMock() } + + // In mock mode, also include any projects published through the backstage + mergePublishedFilmmakerProjects() } /** @@ -130,12 +233,17 @@ export const useContentStore = defineStore('content', () => { error.value = null try { - if (USE_MOCK_DATA) { + const sourceStore = useContentSourceStore() + + if (sourceStore.activeSource === 'indeehub-api' && !USE_MOCK_DATA) { + // Fetch from our self-hosted backend (only when backend is actually running) + await fetchContentFromIndeehubApi() + } else if (USE_MOCK_DATA) { // Use mock data in development or when flag is set await new Promise(resolve => setTimeout(resolve, 100)) fetchContentFromMock() } else { - // Fetch from API + // Fetch from original API await fetchContentFromApi() } } catch (e: any) { diff --git a/src/stores/contentSource.ts b/src/stores/contentSource.ts index 71d38f4..9af90c3 100644 --- a/src/stores/contentSource.ts +++ b/src/stores/contentSource.ts @@ -1,13 +1,35 @@ import { defineStore } from 'pinia' -import { ref, watch } from 'vue' +import { ref, computed, watch } from 'vue' -export type ContentSourceId = 'indeehub' | 'topdocfilms' +export type ContentSourceId = 'indeehub' | 'topdocfilms' | 'indeehub-api' const STORAGE_KEY = 'indeedhub:content-source' export const useContentSourceStore = defineStore('contentSource', () => { const saved = localStorage.getItem(STORAGE_KEY) as ContentSourceId | null - const activeSource = ref(saved === 'topdocfilms' ? 'topdocfilms' : 'indeehub') + const validSources: ContentSourceId[] = ['indeehub', 'topdocfilms', 'indeehub-api'] + const activeSource = ref( + saved && validSources.includes(saved) ? saved : 'indeehub' + ) + + // API source is only available when the backend URL is configured + const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || '' + + const availableSources = computed(() => { + const sources: { id: ContentSourceId; label: string }[] = [ + { id: 'indeehub', label: 'IndeeHub Films' }, + { id: 'topdocfilms', label: 'TopDoc Films' }, + ] + + // Only show API option if backend URL is configured + if (apiUrl) { + sources.push({ id: 'indeehub-api', label: 'IndeeHub API' }) + } + + return sources + }) + + const isApiSource = computed(() => activeSource.value === 'indeehub-api') // Persist to localStorage on change watch(activeSource, (v) => { @@ -19,8 +41,11 @@ export const useContentSourceStore = defineStore('contentSource', () => { } function toggle() { - activeSource.value = activeSource.value === 'indeehub' ? 'topdocfilms' : 'indeehub' + const sources = availableSources.value + const currentIndex = sources.findIndex(s => s.id === activeSource.value) + const nextIndex = (currentIndex + 1) % sources.length + activeSource.value = sources[nextIndex].id } - return { activeSource, setSource, toggle } + return { activeSource, availableSources, isApiSource, setSource, toggle } }) diff --git a/src/style.css b/src/style.css index 506afdb..1ac6a55 100644 --- a/src/style.css +++ b/src/style.css @@ -96,23 +96,16 @@ body { object-fit: cover; } -/* Scrollbar Styles */ +/* Hide scrollbars but keep scrolling */ +html { + overflow-y: scroll; + scrollbar-width: none; /* Firefox */ +} + ::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); + width: 0; + height: 0; + display: none; } /* Netflix-style hero gradient */ diff --git a/src/types/api.ts b/src/types/api.ts index e713a17..be88f7d 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -143,8 +143,16 @@ export interface NostrSessionRequest { } export interface NostrSessionResponse { - token: string - user: ApiUser + /** JWT access token from backend */ + accessToken: string + /** JWT refresh token */ + refreshToken: string + /** Token TTL in seconds */ + expiresIn: number + /** Convenience alias (mapped from accessToken) */ + token?: string + /** User object (populated after separate /auth/me call) */ + user?: ApiUser } // API Response Wrappers @@ -162,6 +170,118 @@ export interface PaginatedResponse { hasMore: boolean } +// Filmmaker / Creator Types +export interface ApiPaymentMethod { + id: string + filmmakerUserId: string + type: 'lightning' | 'bank' + lightningAddress?: string + bankName?: string + accountNumber?: string + routingNumber?: string + withdrawalFrequency: 'manual' | 'weekly' | 'monthly' + isSelected: boolean + createdAt: string + updatedAt: string +} + +export interface ApiFilmmakerAnalytics { + balance: number + totalEarnings: number + myTotalEarnings: number + averageSharePercentage: number +} + +export interface ApiWatchAnalytics { + viewsByDate: Record + trailerViews: number + averageWatchTime: number + streamingRevenueSats: number + rentalRevenueSats: number + purchasesCount: number + purchasesByContent: Array<{ contentId: string; title: string; count: number }> + revenueByDate: Record +} + +export interface ApiPayment { + id: string + amount: number + currency: string + status: 'pending' | 'completed' | 'failed' + type: 'withdrawal' | 'payout' + createdAt: string +} + +export interface ApiCastMember { + id: string + name: string + role: string + profilePictureUrl?: string + type: 'cast' | 'crew' +} + +export interface ApiProjectPermission { + id: string + userId: string + projectId: string + role: 'owner' | 'admin' | 'editor' | 'viewer' | 'revenue-manager' + user?: ApiUser +} + +export interface ApiRevenueSplit { + id: string + userId: string + projectId: string + percentage: number + user?: ApiUser +} + +export interface ApiCoupon { + id: string + code: string + projectId: string + discountType: 'percentage' | 'fixed' + discountValue: number + usageLimit: number + usedCount: number + expiresAt?: string + createdAt: string +} + +export type ProjectStatus = 'draft' | 'published' | 'rejected' +export type ProjectType = 'film' | 'episodic' | 'music-video' + +export interface CreateProjectData { + name: string + type: ProjectType +} + +export interface UpdateProjectData { + name?: string + title?: string + slug?: string + synopsis?: string + status?: ProjectStatus + type?: ProjectType + format?: string + category?: string + poster?: string + trailer?: string + rentalPrice?: number + releaseDate?: string + genres?: string[] + deliveryMode?: 'native' | 'partner' +} + +export interface UploadInitResponse { + UploadId: string + Key: string +} + +export interface UploadPresignedUrlsResponse { + parts: Array<{ PartNumber: number; signedUrl: string }> +} + // Error Types export interface ApiError { message: string diff --git a/src/types/content.ts b/src/types/content.ts index 4fd6244..108db61 100644 --- a/src/types/content.ts +++ b/src/types/content.ts @@ -22,6 +22,10 @@ export interface Content { drmEnabled?: boolean streamingUrl?: string apiData?: any + + // Dual-mode content delivery + deliveryMode?: 'native' | 'partner' + keyUrl?: string } // Nostr event types diff --git a/src/utils/mock.ts b/src/utils/mock.ts new file mode 100644 index 0000000..ac3ff9a --- /dev/null +++ b/src/utils/mock.ts @@ -0,0 +1,63 @@ +/** + * Centralised mock-mode flag. + * + * Rules: + * 1. VITE_USE_MOCK_DATA="true" → mock ON + * 2. VITE_USE_MOCK_DATA="false" → mock OFF (try real backend) + * 3. Unset / empty → fall back to import.meta.env.DEV + * + * If the user opted for real mode (rule 2) but the backend is + * unreachable, `initMockMode()` automatically flips USE_MOCK to + * true so the app remains usable. A console banner lets the dev + * know what happened. + */ + +const explicit = import.meta.env.VITE_USE_MOCK_DATA + +// Exported as `let` so initMockMode() can reassign it. +// ES module live-bindings ensure every importer sees the update. +export let USE_MOCK: boolean = + explicit === 'false' + ? false + : explicit === 'true' + ? true + : import.meta.env.DEV + +/** + * Call once at app startup (before mounting). + * Pings the backend — if unreachable and USE_MOCK is false, + * flips USE_MOCK to true so the whole app falls back to mock data. + */ +export async function initMockMode(): Promise { + // Nothing to check if mock is already on + if (USE_MOCK) return + + const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:4000' + + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 2000) + + await fetch(apiUrl, { + method: 'HEAD', + signal: controller.signal, + }) + + clearTimeout(timeout) + // Backend is reachable — keep USE_MOCK = false + console.log( + '%c✅ Backend connected at %s — real mode active', + 'color: #22c55e; font-weight: bold', + apiUrl, + ) + } catch { + // Backend is not reachable — flip to mock + USE_MOCK = true + console.warn( + '%c⚠️ Backend not reachable at %s — auto-switching to mock mode.\n' + + ' Start the backend (npm run dev:full) for real API/payments.', + 'color: #f59e0b; font-weight: bold', + apiUrl, + ) + } +} diff --git a/src/views/Profile.vue b/src/views/Profile.vue index 6641687..54e7f4b 100644 --- a/src/views/Profile.vue +++ b/src/views/Profile.vue @@ -101,8 +101,8 @@
{{ user.filmmaker.professionalName }}
- @@ -164,9 +164,12 @@ const { logout: nostrLogout } = useAccounts() const contentSourceStore = useContentSourceStore() const contentStore = useContentStore() -const contentSourceLabel = computed(() => - contentSourceStore.activeSource === 'indeehub' ? 'IndeeHub Films' : 'TopDoc Films' -) +const contentSourceLabel = computed(() => { + const source = contentSourceStore.availableSources.find( + s => s.id === contentSourceStore.activeSource + ) + return source?.label || 'IndeeHub Films' +}) function handleSourceToggle() { contentSourceStore.toggle() diff --git a/src/views/backstage/Analytics.vue b/src/views/backstage/Analytics.vue new file mode 100644 index 0000000..4c1f9f8 --- /dev/null +++ b/src/views/backstage/Analytics.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/src/views/backstage/Backstage.vue b/src/views/backstage/Backstage.vue new file mode 100644 index 0000000..089258d --- /dev/null +++ b/src/views/backstage/Backstage.vue @@ -0,0 +1,558 @@ + + + + + diff --git a/src/views/backstage/ProjectEditor.vue b/src/views/backstage/ProjectEditor.vue new file mode 100644 index 0000000..584b5f4 --- /dev/null +++ b/src/views/backstage/ProjectEditor.vue @@ -0,0 +1,535 @@ + + + + + diff --git a/src/views/backstage/Settings.vue b/src/views/backstage/Settings.vue new file mode 100644 index 0000000..ba4b3d9 --- /dev/null +++ b/src/views/backstage/Settings.vue @@ -0,0 +1,537 @@ + + + + + diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 5992e0f..1bdf83f 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/env.d.ts","./src/main.ts","./src/composables/useaccess.ts","./src/composables/useaccounts.ts","./src/composables/useauth.ts","./src/composables/usecontentdiscovery.ts","./src/composables/usemobile.ts","./src/composables/usenostr.ts","./src/composables/useobservable.ts","./src/composables/usetoast.ts","./src/config/api.config.ts","./src/data/indeehubfilms.ts","./src/data/testpersonas.ts","./src/data/topdocfilms.ts","./src/lib/accounts.ts","./src/lib/nostr.ts","./src/lib/relay.ts","./src/router/guards.ts","./src/router/index.ts","./src/services/api.service.ts","./src/services/auth.service.ts","./src/services/content.service.ts","./src/services/library.service.ts","./src/services/subscription.service.ts","./src/stores/auth.ts","./src/stores/content.ts","./src/stores/contentsource.ts","./src/stores/searchselection.ts","./src/types/api.ts","./src/types/content.ts","./src/utils/indeehubapi.ts","./src/utils/mappers.ts","./src/utils/nostr.ts","./src/app.vue","./src/components/appheader.vue","./src/components/authmodal.vue","./src/components/commentnode.vue","./src/components/contentdetailmodal.vue","./src/components/contentrow.vue","./src/components/mobilenav.vue","./src/components/mobilesearch.vue","./src/components/rentalmodal.vue","./src/components/splashintro.vue","./src/components/splashintroicon.vue","./src/components/subscriptionmodal.vue","./src/components/toastcontainer.vue","./src/components/videoplayer.vue","./src/views/browse.vue","./src/views/profile.vue"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/env.d.ts","./src/main.ts","./src/composables/useaccess.ts","./src/composables/useaccounts.ts","./src/composables/useauth.ts","./src/composables/usecontentdiscovery.ts","./src/composables/usefilmmaker.ts","./src/composables/usemobile.ts","./src/composables/usenostr.ts","./src/composables/useobservable.ts","./src/composables/usetoast.ts","./src/composables/useupload.ts","./src/config/api.config.ts","./src/data/indeehubfilms.ts","./src/data/testpersonas.ts","./src/data/topdocfilms.ts","./src/lib/accounts.ts","./src/lib/nostr.ts","./src/lib/relay.ts","./src/router/guards.ts","./src/router/index.ts","./src/services/api.service.ts","./src/services/auth.service.ts","./src/services/content.service.ts","./src/services/filmmaker.service.ts","./src/services/indeehub-api.service.ts","./src/services/library.service.ts","./src/services/nip98.service.ts","./src/services/subscription.service.ts","./src/stores/auth.ts","./src/stores/content.ts","./src/stores/contentsource.ts","./src/stores/searchselection.ts","./src/types/api.ts","./src/types/content.ts","./src/utils/indeehubapi.ts","./src/utils/mappers.ts","./src/utils/nostr.ts","./src/app.vue","./src/components/appheader.vue","./src/components/authmodal.vue","./src/components/commentnode.vue","./src/components/contentdetailmodal.vue","./src/components/contentrow.vue","./src/components/mobilenav.vue","./src/components/mobilesearch.vue","./src/components/rentalmodal.vue","./src/components/splashintro.vue","./src/components/splashintroicon.vue","./src/components/subscriptionmodal.vue","./src/components/toastcontainer.vue","./src/components/videoplayer.vue","./src/components/backstage/assetstab.vue","./src/components/backstage/castcrewtab.vue","./src/components/backstage/contenttab.vue","./src/components/backstage/couponstab.vue","./src/components/backstage/detailstab.vue","./src/components/backstage/documentationtab.vue","./src/components/backstage/permissionstab.vue","./src/components/backstage/revenuetab.vue","./src/components/backstage/uploadzone.vue","./src/views/browse.vue","./src/views/profile.vue","./src/views/backstage/analytics.vue","./src/views/backstage/backstage.vue","./src/views/backstage/projecteditor.vue","./src/views/backstage/settings.vue"],"version":"5.9.3"} \ No newline at end of file