From 5846da5be4ff4dbddcd6904d2c9e4309e1f2f588 Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:11:35 +0100 Subject: [PATCH 1/8] restored dependencies and bootstrap lost in the swagger merge --- package-lock.json | 520 ++++++++++++++++++++++++++++++++++++---------- package.json | 9 +- src/app.module.ts | 1 + src/main.ts | 30 ++- 4 files changed, 441 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8978e30..3b9549b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,14 @@ "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.2.6", "@nestjs/schedule": "^5.0.1", - "@nestjs/swagger": "^12.0.2", + "@nestjs/swagger": "^11.4.7", + "@opentelemetry/exporter-trace-otlp-http": "^0.222.0", + "@opentelemetry/instrumentation-express": "^0.70.0", + "@opentelemetry/instrumentation-http": "^0.222.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.68.0", + "@opentelemetry/sdk-node": "^0.222.0", "@prisma/client": "6.19.3", "@stellar/stellar-sdk": "^16.2.0", "bcrypt": "^6.0.0", @@ -2121,9 +2126,9 @@ } }, "node_modules/@microsoft/tsdoc": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.17.0.tgz", - "integrity": "sha512-p68VexhnH7ojf3U27RdryUDqnKJgKaWUbCL96vJR7N0mqDuhKG5OIECk3sCPPdynxjk+yoVhRkScqtv5KQjzsw==", + "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/@napi-rs/wasm-runtime": { @@ -2451,15 +2456,12 @@ } }, "node_modules/@nestjs/mapped-types": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-12.0.0.tgz", - "integrity": "sha512-aX7lxZcqXldtnSr0bgT2ZPQvpTpIwBMW1GMX29o4s3q02vmq6JoTYuPW0GwYXa8BiFMRX7WbGZVDA703pNqOMQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, "peerDependencies": { - "@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0", + "@nestjs/common": "^10.0.0 || ^11.0.0", "class-transformer": "^0.4.0 || ^0.5.0", "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", "reflect-metadata": "^0.1.12 || ^0.2.0" @@ -2484,14 +2486,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.28", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz", - "integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.6.tgz", + "integrity": "sha512-jPypI/+aueizkYG2Bw2kg8GaNsWE7ekFBSQ/Rwet7wEdBA/+q2FA364UbDabqjlR7C3VSNHmP/JXh+8+jXyDOw==", "license": "MIT", "dependencies": { "cors": "2.8.6", "express": "5.2.1", - "multer": "2.2.0", + "multer": "2.4.0", "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, @@ -2622,30 +2624,25 @@ } }, "node_modules/@nestjs/swagger": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-12.0.2.tgz", - "integrity": "sha512-/IDj6o2k1wqZybhIkMaJp58YNrAos/4TwTf3zBn/j1flKpMb5Tyfbj9sP6rpLucQ7PzHHu7n4ucmeHWEisfn6A==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.7.tgz", + "integrity": "sha512-QyDYnmfP4IRucgmtQxMqzgRBdWtjFoDp8eFvvgf92+3wdLCL+Q0xOFO1948j/ntW/Wi7qT2dyck6ka8ADzPWQQ==", "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "0.17.0", - "@nestjs/mapped-types": "12.0.0", - "@standard-schema/spec": "1.1.0", - "es-toolkit": "1.52.0", - "js-yaml": "5.4.2", + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "5.3.0", + "lodash": "4.18.1", "path-to-regexp": "8.4.2", - "swagger-ui-dist": "5.33.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "swagger-ui-dist": "5.32.13" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/common": "^12.0.0", - "@nestjs/core": "^12.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12 || ^0.2.0", - "typescript": "^5.5.0 || ^6.0.0" + "reflect-metadata": "^0.1.12 || ^0.2.0" }, "peerDependenciesMeta": { "@fastify/static": { @@ -2656,16 +2653,13 @@ }, "class-validator": { "optional": true - }, - "typescript": { - "optional": true } } }, "node_modules/@nestjs/swagger/node_modules/js-yaml": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", - "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.3.0.tgz", + "integrity": "sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==", "funding": [ { "type": "github", @@ -2684,6 +2678,15 @@ "js-yaml": "bin/js-yaml.mjs" } }, + "node_modules/@nestjs/swagger/node_modules/swagger-ui-dist": { + "version": "5.32.13", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.13.tgz", + "integrity": "sha512-qQobzb3DeC2LeK0j3E8812Ef4aIq1y9flJxvZkimkqUC/w4u7wS+yCc+VakqGJLweUUBrI24effhwo8OsAvNAw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/@nestjs/testing": { "version": "11.1.28", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.28.tgz", @@ -3367,7 +3370,7 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", @@ -3380,14 +3383,14 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3401,14 +3404,14 @@ "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "6.19.3", @@ -3420,12 +3423,69 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "6.19.3" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -3464,6 +3524,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, "license": "MIT" }, "node_modules/@stellar/js-xdr": { @@ -5336,6 +5397,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/busboy": { @@ -5362,7 +5424,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.3", @@ -5391,7 +5453,7 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5508,7 +5570,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -5550,7 +5612,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "consola": "^3.2.3" @@ -5839,33 +5901,18 @@ "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/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -6054,7 +6101,7 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -6077,7 +6124,7 @@ "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -6102,7 +6149,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-newline": { @@ -6218,7 +6265,7 @@ "version": "3.21.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -6255,7 +6302,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14" @@ -6359,18 +6406,6 @@ "node": ">= 0.4" } }, - "node_modules/es-toolkit": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", - "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks", - "tests/types", - "tests/browser-compat" - ] - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6767,14 +6802,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -6797,7 +6832,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -7294,7 +7329,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "citty": "^0.1.6", @@ -8681,7 +8716,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -9759,6 +9794,174 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.12.0.tgz", + "integrity": "sha512-PCN1x1OzFeFB2GlP91s46q7Gz8Jf16Z7M45bzpoqrYZlh12QeY6tYVM5h299wbwy1DxnQ69VFmmh8Hb2Ff8mbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/minimizer-webpack-plugin/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", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/minimizer-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", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/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", + "peer": true + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimizer-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", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -9782,14 +9985,13 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", - "concat-stream": "^2.0.0", "type-is": "^1.6.18" }, "engines": { @@ -9922,7 +10124,7 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-gyp-build": { @@ -9980,7 +10182,7 @@ "version": "0.6.9", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "citty": "^0.2.2", @@ -9998,7 +10200,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -10026,7 +10228,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/on-finished": { @@ -10315,7 +10517,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/pause": { @@ -10327,7 +10529,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { @@ -10446,7 +10648,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.2.4", @@ -10536,7 +10738,7 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -10678,7 +10880,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", @@ -10705,6 +10907,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -10719,7 +10922,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -11216,6 +11419,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -11697,7 +11901,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -12029,17 +12233,11 @@ "url": "https://opencollective.com/express" } }, - "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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12219,6 +12417,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -12303,6 +12502,48 @@ "defaults": "^1.0.3" } }, + "node_modules/webpack": { + "version": "5.111.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.111.1.tgz", + "integrity": "sha512-cNypaz0RP+S4cvQVi1M/3bRUkvNP0xZpL0TjFx+c6jg64VbxD+2weWDgY9UnjRI6dhZgtaj8DU76GGFcJ79xPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@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", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.25.0", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "schema-utils": "^4.5.0", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "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", @@ -12323,6 +12564,67 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/webpack/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", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/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", + "peer": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index be5e429..0d87e5a 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,14 @@ "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.2.6", "@nestjs/schedule": "^5.0.1", - "@nestjs/swagger": "^12.0.2", + "@nestjs/swagger": "^11.4.7", + "@opentelemetry/exporter-trace-otlp-http": "^0.222.0", + "@opentelemetry/instrumentation-express": "^0.70.0", + "@opentelemetry/instrumentation-http": "^0.222.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.68.0", + "@opentelemetry/sdk-node": "^0.222.0", "@prisma/client": "6.19.3", "@stellar/stellar-sdk": "^16.2.0", "bcrypt": "^6.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index 1bbd16d..e0eb02c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,7 @@ import { AuditModule } from './audit/audit.module'; import { CacheModule } from './common/cache/cache.module'; import { SchedulerModule } from './scheduler/scheduler.module'; import { WebhooksModule } from './webhooks/webhooks.module'; +import { TracingShutdownService } from './tracing'; import { BigIntSerializerInterceptor } from './common/interceptors/bigint-serializer.interceptor'; import { RequestTimeoutInterceptor } from './common/interceptors/request-timeout.interceptor'; diff --git a/src/main.ts b/src/main.ts index a7f335c..c8b3d73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,5 @@ -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe, VersioningType } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import helmet from 'helmet'; -import { AppModule } from './app.module'; -import { GlobalExceptionFilter } from './common/filters/global-exception.filter'; +import 'dotenv/config'; +import { startTracing } from './tracing'; async function bootstrap() { // Instrumentation must start before Nest and Express are loaded. @@ -15,18 +10,37 @@ async function bootstrap() { { ValidationPipe, VersioningType }, { ConfigService }, { default: helmet }, + { DocumentBuilder, SwaggerModule }, + { GlobalExceptionFilter }, ] = await Promise.all([ import('@nestjs/core'), import('./app.module.js'), import('@nestjs/common'), import('@nestjs/config'), import('helmet'), + import('@nestjs/swagger'), + import('./common/filters/global-exception.filter.js'), ]); const app = await NestFactory.create(AppModule); if (tracing) app.enableShutdownHooks(); const config = app.get(ConfigService); - app.use(helmet()); + const cspDirectives = config.get('CSP_DIRECTIVES'); + const helmetOptions: Record = {}; + if (cspDirectives) { + try { + helmetOptions.contentSecurityPolicy = { + directives: JSON.parse(cspDirectives) as Record, + }; + } catch { + helmetOptions.contentSecurityPolicy = true; + } + } + app.use(helmet(helmetOptions)); + + const bodyLimit = config.get('JSON_BODY_LIMIT', '100kb'); + const express = await import('express'); + app.use(express.json({ limit: bodyLimit })); const corsOrigins = config .getOrThrow('CORS_ORIGINS') From c418f3197838b71027de7aef8706423d1dc0977f Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:11:42 +0100 Subject: [PATCH 2/8] restored the PendingTx model dropped from the Prisma schema --- prisma/schema.prisma | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d1eae6c..c5bb199 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,6 +90,7 @@ model User { waitlistEntries WaitlistEntry[] promoRedemptions PromoCodeRedemption[] auditLogs AuditLog[] + pendingTxs PendingTx[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -374,4 +375,27 @@ model AuditLog { @@index([actorId]) @@index([entityType, entityId]) @@index([createdAt]) -} \ No newline at end of file +} + +/// Records intent between a build-transaction call (e.g. `buildPurchaseTx`) +/// and its confirm call, so the window where a signed XDR exists but hasn't +/// been submitted/confirmed isn't a total black box. Rows are transient and +/// swept by `PendingTxCleanupService` once `expiresAt` has passed. +model PendingTx { + id String @id @default(uuid()) + /// Discriminator for which build-transaction flow created this row, e.g. + /// "issue", "purchase", "transfer", "check-in", "revoke", "list-resale", + /// "cancel-resale", "buy-resale". + type String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + /// Arbitrary JSON snapshot of the parameters used to build the transaction, + /// so a confirm call (or a support investigation) can inspect what was + /// intended without re-deriving it. + payload Json + expiresAt DateTime + createdAt DateTime @default(now()) + + @@index([expiresAt]) + @@index([userId]) +} From 6b79eae6e1ccba6edf5c757464671d2fa3ddd0e9 Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:11:43 +0100 Subject: [PATCH 3/8] fixed strict-mode type errors in specs, factories and the scanner guard --- src/common/decorators/is-bigint-string.decorator.spec.ts | 2 +- .../decorators/is-stellar-public-key.decorator.spec.ts | 2 +- src/scanner-devices/guards/scanner-device.guard.ts | 2 +- src/tickets/dto/transfer-ticket.dto.spec.ts | 2 +- src/tickets/dto/update-resale-price.dto.ts | 2 +- src/tickets/offline-token.service.spec.ts | 7 ++----- test/factories/event.factory.ts | 2 +- test/factories/org.factory.ts | 2 +- 8 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/common/decorators/is-bigint-string.decorator.spec.ts b/src/common/decorators/is-bigint-string.decorator.spec.ts index 350d8f2..b62be9c 100644 --- a/src/common/decorators/is-bigint-string.decorator.spec.ts +++ b/src/common/decorators/is-bigint-string.decorator.spec.ts @@ -3,7 +3,7 @@ import { IsBigIntString } from './is-bigint-string.decorator'; class TestDto { @IsBigIntString() - price: string; + price!: string; } describe('IsBigIntString', () => { diff --git a/src/common/decorators/is-stellar-public-key.decorator.spec.ts b/src/common/decorators/is-stellar-public-key.decorator.spec.ts index 383eb31..eafe33c 100644 --- a/src/common/decorators/is-stellar-public-key.decorator.spec.ts +++ b/src/common/decorators/is-stellar-public-key.decorator.spec.ts @@ -3,7 +3,7 @@ import { IsStellarPublicKey } from './is-stellar-public-key.decorator'; class TestDto { @IsStellarPublicKey() - stellarAccount: string; + stellarAccount!: string; } describe('IsStellarPublicKey', () => { diff --git a/src/scanner-devices/guards/scanner-device.guard.ts b/src/scanner-devices/guards/scanner-device.guard.ts index 156bf93..20d94c3 100644 --- a/src/scanner-devices/guards/scanner-device.guard.ts +++ b/src/scanner-devices/guards/scanner-device.guard.ts @@ -29,7 +29,7 @@ export class ScannerDeviceGuard implements CanActivate { const device = await this.scannerDevices.authenticate(token); const ticketId = request.params.ticketId; - if (ticketId) { + if (typeof ticketId === 'string') { const ticket = await this.prisma.ticket.findUnique({ where: { id: ticketId }, select: { eventId: true }, diff --git a/src/tickets/dto/transfer-ticket.dto.spec.ts b/src/tickets/dto/transfer-ticket.dto.spec.ts index fbf7b85..ba55cc5 100644 --- a/src/tickets/dto/transfer-ticket.dto.spec.ts +++ b/src/tickets/dto/transfer-ticket.dto.spec.ts @@ -32,7 +32,7 @@ describe('TransferTicketDto', () => { it('rejects a bad recipient public key', async () => { const dto = plainToInstance(TransferTicketDto, { toUserId: UUID, - toPublicKey: 'G' + 'A' * 55, + toPublicKey: 'G' + 'A'.repeat(55), }); const errors = await validate(dto); expect(errors.some((e) => e.property === 'toPublicKey')).toBe(true); diff --git a/src/tickets/dto/update-resale-price.dto.ts b/src/tickets/dto/update-resale-price.dto.ts index 6819494..2e9aeba 100644 --- a/src/tickets/dto/update-resale-price.dto.ts +++ b/src/tickets/dto/update-resale-price.dto.ts @@ -3,5 +3,5 @@ import { IsBigIntString } from '../../common/decorators/is-bigint-string.decorat export class UpdateResalePriceDto { /** Updated asking price in settlement token smallest units. */ @IsBigIntString() - price: string; + price!: string; } diff --git a/src/tickets/offline-token.service.spec.ts b/src/tickets/offline-token.service.spec.ts index 3e148f0..24bd0e8 100644 --- a/src/tickets/offline-token.service.spec.ts +++ b/src/tickets/offline-token.service.spec.ts @@ -1,11 +1,8 @@ -import { generateKeyPairSync } from 'node:crypto'; +import { generateKeyPairSync, type KeyObject } from 'node:crypto'; import type { ConfigService } from '@nestjs/config'; import { OfflineTokenService } from './offline-token.service'; -function pem( - key: { export: (opts: unknown) => string | Buffer }, - type: 'pkcs8' | 'spki', -) { +function pem(key: KeyObject, type: 'pkcs8' | 'spki') { return key.export({ type, format: 'pem' }) as string; } diff --git a/test/factories/event.factory.ts b/test/factories/event.factory.ts index f5985df..23e5af3 100644 --- a/test/factories/event.factory.ts +++ b/test/factories/event.factory.ts @@ -22,7 +22,7 @@ export function createEvent(overrides: Partial = {}): Event { createdAt: new Date('2026-01-01T00:00:00.000Z'), updatedAt: new Date('2026-01-01T00:00:00.000Z'), // #207 — soft-delete marker (null = live). - ...({ deletedAt: null } as Partial), + deletedAt: null, ...overrides, }; } diff --git a/test/factories/org.factory.ts b/test/factories/org.factory.ts index 9003180..b58b636 100644 --- a/test/factories/org.factory.ts +++ b/test/factories/org.factory.ts @@ -18,7 +18,7 @@ export function createOrganization( createdAt: new Date('2026-01-01T00:00:00.000Z'), updatedAt: new Date('2026-01-01T00:00:00.000Z'), // #207 — soft-delete marker (null = live). - ...({ deletedAt: null } as Partial), + deletedAt: null, ...overrides, }; } From e3a72c05dc4e9a154d3d41f51b8ad1ebb575297a Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:11:51 +0100 Subject: [PATCH 4/8] declared the body limit, CSP and cache TTL env vars and regenerated docs --- docs/CONFIGURATION.md | 5 ++++- docs/bruno/events/Restore.bru | 13 +++++++++++++ docs/bruno/events/Soft Delete.bru | 13 +++++++++++++ docs/bruno/organizations/Restore.bru | 13 +++++++++++++ docs/bruno/organizations/Soft Delete.bru | 13 +++++++++++++ src/config/env.validation.ts | 19 +++++++++++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 docs/bruno/events/Restore.bru create mode 100644 docs/bruno/events/Soft Delete.bru create mode 100644 docs/bruno/organizations/Restore.bru create mode 100644 docs/bruno/organizations/Soft Delete.bru diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 783c146..bba93bc 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -23,13 +23,16 @@ run with a missing or malformed value. See | `DATABASE_URL` | string | **required** | — | yes | PostgreSQL connection string for the Prisma client. This is the single system of record; see docs/DATABASE.md. | `RATE_LIMIT_STORE` | `memory \| redis` | optional | `memory` | yes | Where rate-limit counters live. `memory` (default) is per-process; `redis` shares them across instances. See docs/RATE_LIMITING.md. | `CACHE_DRIVER` | `memory \| redis` | optional | `memory` | yes | Where cached entries live. `memory` (default) is per-process; `redis` shares them across instances. See docs/CACHING.md. +| `CACHE_TTL_SECONDS` | integer | optional | `60` \* | yes | Default time-to-live, in seconds, for entries written by the response cache interceptor on public event listings. Default 60. See docs/CACHING.md. | `WEBHOOK_QUEUE_ENABLED` | `true \| false` | optional | `false` | yes | Enables the BullMQ-backed outbound webhook queue. Kept as the literal strings 'true'/'false' (like the feature flags) because implicit conversion would turn the string 'false' into boolean true. Off unless 'true'. See docs/WEBHOOKS.md. | `WEBHOOK_QUEUE_ATTEMPTS` | integer | optional | `5` \* | yes | Total delivery attempts per webhook, the first included. Default 5. | `WEBHOOK_QUEUE_BACKOFF_MS` | integer | optional | `5000` \* | yes | Delay before the first webhook retry, doubling on each further retry. Default 5000. | `SCHEDULER_ENABLED` | `true \| false` | optional | `true` | yes | Set to 'false' to switch off every cron job registered through SchedulerModule. On unless 'false'. See docs/SCHEDULER.md. | `REDIS_URL` | string | conditional | — | yes (when required) | Redis connection URL (e.g. redis://localhost:6379). Required when RATE_LIMIT_STORE=redis, CACHE_DRIVER=redis or WEBHOOK_QUEUE_ENABLED=true. | `JWT_SECRET` | string (min 32 chars) | **required** | — | yes | Secret used to sign and verify JWT access tokens. Must be at least 32 characters. Rotating it invalidates every issued token. See docs/AUTHENTICATION.md. -| `APP_URL` | string | **required** | `http://localhost:3001` | yes | Public origin this API is reached at, e.g. http://localhost:3001. Used as the CORS allow-list and to build links in outbound email/webhooks. +| `CORS_ORIGINS` | string | **required** | `http://localhost:3001` | yes | Comma-separated list of allowed CORS origins (e.g. "https://app.example.com,https://staging.example.com"). Wildcard (*) is NOT allowed in production. Used as the CORS allow-list and to build links in outbound email/webhooks. +| `JSON_BODY_LIMIT` | string | optional | `100kb` | yes | Maximum accepted JSON request body, as an Express size string such as 100kb or 1mb. Larger payloads are rejected with 413. Default 100kb. +| `CSP_DIRECTIVES` | string | optional | `{"defaultSrc":["'self'"],"scriptSrc":["'self'"]}` \* | yes | Helmet Content-Security-Policy directives as a JSON object string, e.g. {"defaultSrc":["'self'"]}. Leave unset to keep helmet's defaults. | `SOROBAN_RPC_URL` | string | **required** | — | yes | Soroban RPC endpoint the StellarService submits contract calls through. | `STELLAR_NETWORK` | `testnet \| futurenet \| mainnet` | **required** | `testnet` | yes | Which Stellar network every contract call targets. Must match the network the `ticketing` contract is deployed on and the one user wallets are set to, or every submit will fail. | `TICKETING_CONTRACT_ID` | string | **required** | — | yes | Deployed `ticketing` contract's C... address. diff --git a/docs/bruno/events/Restore.bru b/docs/bruno/events/Restore.bru new file mode 100644 index 0000000..e70eb5a --- /dev/null +++ b/docs/bruno/events/Restore.bru @@ -0,0 +1,13 @@ +meta { + name: Restore + type: http +} + +post { + url: {{baseUrl}}/v1/events/{{eventId}}/restore + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} diff --git a/docs/bruno/events/Soft Delete.bru b/docs/bruno/events/Soft Delete.bru new file mode 100644 index 0000000..1affb45 --- /dev/null +++ b/docs/bruno/events/Soft Delete.bru @@ -0,0 +1,13 @@ +meta { + name: Soft Delete + type: http +} + +delete { + url: {{baseUrl}}/v1/events/{{eventId}} + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} diff --git a/docs/bruno/organizations/Restore.bru b/docs/bruno/organizations/Restore.bru new file mode 100644 index 0000000..461ccb2 --- /dev/null +++ b/docs/bruno/organizations/Restore.bru @@ -0,0 +1,13 @@ +meta { + name: Restore + type: http +} + +post { + url: {{baseUrl}}/v1/organizations/{{id}}/restore + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} diff --git a/docs/bruno/organizations/Soft Delete.bru b/docs/bruno/organizations/Soft Delete.bru new file mode 100644 index 0000000..9f50ed1 --- /dev/null +++ b/docs/bruno/organizations/Soft Delete.bru @@ -0,0 +1,13 @@ +meta { + name: Soft Delete + type: http +} + +delete { + url: {{baseUrl}}/v1/organizations/{{id}} + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} diff --git a/src/config/env.validation.ts b/src/config/env.validation.ts index 13cb0de..fd935ab 100644 --- a/src/config/env.validation.ts +++ b/src/config/env.validation.ts @@ -59,6 +59,13 @@ class EnvironmentVariables { @IsOptional() CACHE_DRIVER?: string; + /// Default time-to-live, in seconds, for entries written by the response + /// cache interceptor on public event listings. Default 60. See + /// docs/CACHING.md. + @IsInt() + @IsOptional() + CACHE_TTL_SECONDS?: number; + /// Enables the BullMQ-backed outbound webhook queue. Kept as the literal /// strings 'true'/'false' (like the feature flags) because implicit /// conversion would turn the string 'false' into boolean true. Off unless @@ -108,6 +115,18 @@ class EnvironmentVariables { @IsString() CORS_ORIGINS!: string; + /// Maximum accepted JSON request body, as an Express size string such as + /// 100kb or 1mb. Larger payloads are rejected with 413. Default 100kb. + @IsString() + @IsOptional() + JSON_BODY_LIMIT?: string; + + /// Helmet Content-Security-Policy directives as a JSON object string, e.g. + /// {"defaultSrc":["'self'"]}. Leave unset to keep helmet's defaults. + @IsString() + @IsOptional() + CSP_DIRECTIVES?: string; + /// Soroban RPC endpoint the StellarService submits contract calls through. @IsString() SOROBAN_RPC_URL!: string; From 4c0ca2826c1f47a26fd91fda33c0191d0164f968 Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:11:57 +0100 Subject: [PATCH 5/8] passed HttpExceptions through the global exception filter unchanged --- .../filters/global-exception.filter.spec.ts | 51 ++++++++++++++++++- src/common/filters/global-exception.filter.ts | 18 ++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/common/filters/global-exception.filter.spec.ts b/src/common/filters/global-exception.filter.spec.ts index a0b3e8e..f637d3c 100644 --- a/src/common/filters/global-exception.filter.spec.ts +++ b/src/common/filters/global-exception.filter.spec.ts @@ -1,4 +1,9 @@ -import { HttpStatus } from '@nestjs/common'; +import { + BadRequestException, + HttpException, + HttpStatus, + NotFoundException, +} from '@nestjs/common'; import type { ArgumentsHost } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { GlobalExceptionFilter } from './global-exception.filter'; @@ -39,6 +44,50 @@ describe('GlobalExceptionFilter', () => { }); } + describe('HTTP exceptions', () => { + it('forwards a NotFoundException with its status and Nest body shape', () => { + const { argumentsHost, response } = host(); + filter.catch(new NotFoundException('Ticket not found'), argumentsHost); + expect(response.status).toHaveBeenCalledWith(HttpStatus.NOT_FOUND); + expect(response.json).toHaveBeenCalledWith({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Ticket not found', + error: 'Not Found', + }); + }); + + it('keeps ValidationPipe-style message arrays intact', () => { + const { argumentsHost, response } = host(); + filter.catch( + new BadRequestException([ + 'price must be a non-negative integer string', + ]), + argumentsHost, + ); + expect(response.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(response.json).toHaveBeenCalledWith({ + statusCode: HttpStatus.BAD_REQUEST, + message: ['price must be a non-negative integer string'], + error: 'Bad Request', + }); + }); + + it('wraps a bare string response body', () => { + const { argumentsHost, response } = host(); + filter.catch( + new HttpException('slow down', HttpStatus.TOO_MANY_REQUESTS), + argumentsHost, + ); + expect(response.status).toHaveBeenCalledWith( + HttpStatus.TOO_MANY_REQUESTS, + ); + expect(response.json).toHaveBeenCalledWith({ + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: 'slow down', + }); + }); + }); + describe('Prisma errors', () => { it('maps P2002 to 409 Conflict', () => { const { argumentsHost, response } = host(); diff --git a/src/common/filters/global-exception.filter.ts b/src/common/filters/global-exception.filter.ts index fe9c7dd..ee9832f 100644 --- a/src/common/filters/global-exception.filter.ts +++ b/src/common/filters/global-exception.filter.ts @@ -2,9 +2,11 @@ import { ArgumentsHost, Catch, ExceptionFilter, + HttpException, HttpStatus, Logger, } from '@nestjs/common'; +import type { Request, Response } from 'express'; import { Prisma } from '@prisma/client'; import { DomainError } from '../errors/domain.error'; @@ -24,8 +26,20 @@ export class GlobalExceptionFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost): void { const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + // HttpExceptions (404s, 403s, ValidationPipe 400s, ...) already carry + // their status and body. Forward them unchanged so clients keep Nest's + // `{ statusCode, message, error }` shape instead of a generic 500. + if (exception instanceof HttpException) { + const statusCode = exception.getStatus(); + const body = exception.getResponse(); + response + .status(statusCode) + .json(typeof body === 'string' ? { statusCode, message: body } : body); + return; + } let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; let code = 'INTERNAL_ERROR'; From c3f350cce31d0de382cec69a44289c199623f86b Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:12:04 +0100 Subject: [PATCH 6/8] fixed the events controller and capacity lock specs --- src/events/events.controller.spec.ts | 2 ++ src/tickets/tickets.service.spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/events/events.controller.spec.ts b/src/events/events.controller.spec.ts index fd4ca31..754e56c 100644 --- a/src/events/events.controller.spec.ts +++ b/src/events/events.controller.spec.ts @@ -13,6 +13,7 @@ import { } from '../../test/helpers/controller-app'; import { EventsController } from './events.controller'; import { EventsService } from './events.service'; +import { ResponseCacheInterceptor } from '../common/interceptors/response-cache.interceptor'; describe('EventsController', () => { let app: INestApplication; @@ -33,6 +34,7 @@ describe('EventsController', () => { app = await createControllerApp({ controller: EventsController, service: { provide: EventsService, useValue: service }, + passthrough: { interceptors: [ResponseCacheInterceptor] }, }); }); afterAll(() => app.close()); diff --git a/src/tickets/tickets.service.spec.ts b/src/tickets/tickets.service.spec.ts index 69fd1a5..bc734d4 100644 --- a/src/tickets/tickets.service.spec.ts +++ b/src/tickets/tickets.service.spec.ts @@ -280,8 +280,8 @@ describe('TicketsService', () => { // The row lock precedes the increment inside the same transaction. expect(prisma.$queryRaw).toHaveBeenCalledTimes(1); - const [query] = prisma.$queryRaw.mock.calls[0]; - expect(query[0]).toContain('FOR UPDATE'); + const [query] = prisma.$queryRaw.mock.calls[0] as [TemplateStringsArray]; + expect(query.join('')).toContain('FOR UPDATE'); expect(prisma.$queryRaw.mock.invocationCallOrder[0]).toBeLessThan( prisma.ticketType.update.mock.invocationCallOrder[0], ); @@ -1160,7 +1160,7 @@ describe('TicketsService', () => { expect.objectContaining({ data: expect.objectContaining({ checkInReason: 'scanner_malfunction', - }), + }) as object, }), ); }); @@ -1183,7 +1183,7 @@ describe('TicketsService', () => { expect.objectContaining({ data: expect.objectContaining({ checkInReason: null, - }), + }) as object, }), ); }); From 152a9e1f450cec8b0f3df87db44c9d3b6b92fd2c Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:12:12 +0100 Subject: [PATCH 7/8] made the audit gate follow transitive findings and allowlisted the Prisma CLI advisory --- scripts/audit-allowlist.json | 7 ++++++- scripts/check-audit.js | 27 +++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/scripts/audit-allowlist.json b/scripts/audit-allowlist.json index fe51488..76362e7 100644 --- a/scripts/audit-allowlist.json +++ b/scripts/audit-allowlist.json @@ -1 +1,6 @@ -[] +[ + { + "id": "1145093", + "reason": "GHSA-ggr8-5vv4-36mx (deepmerge-ts stack exhaustion on recursive object graphs). Reached only through @prisma/config, which the Prisma CLI loads when running generate/migrate; no request-time code path merges untrusted input. Prisma 6.19.3 is the newest 6.x release and still pins deepmerge-ts 7.1.5; the fixed 8.x line only ships with Prisma 7 (see docs/PRISMA_7_NOTE.md). Remove once the project moves to Prisma 7." + } +] diff --git a/scripts/check-audit.js b/scripts/check-audit.js index 8581cbb..446a31c 100644 --- a/scripts/check-audit.js +++ b/scripts/check-audit.js @@ -40,19 +40,38 @@ function runAudit() { } } +/** + * Collects the advisory ids behind a finding. `npm audit` reports a package + * that is only vulnerable through a dependency with the dependency's *name* + * in `via`, not the advisory, so follow those names down to the advisories + * that actually caused the finding. An allowlisted advisory then clears + * every package it reached. + */ +function advisoryIds(advisory, vulnerabilities, seen = new Set()) { + const ids = []; + for (const via of advisory.via ?? []) { + if (typeof via === "object" && via.source) { + ids.push(String(via.source)); + } else if (typeof via === "string" && !seen.has(via) && vulnerabilities[via]) { + seen.add(via); + ids.push(...advisoryIds(vulnerabilities[via], vulnerabilities, seen)); + } + } + return ids; +} + function main() { const allowlist = loadAllowlist(); const report = runAudit(); - const advisories = Object.values(report.vulnerabilities ?? {}); + const vulnerabilities = report.vulnerabilities ?? {}; + const advisories = Object.values(vulnerabilities); const failures = []; for (const advisory of advisories) { const severity = advisory.severity; if ((SEVERITY_RANK[severity] ?? 0) < SEVERITY_RANK[THRESHOLD]) continue; - const ids = (advisory.via ?? []) - .filter((v) => typeof v === "object" && v.source) - .map((v) => String(v.source)); + const ids = advisoryIds(advisory, vulnerabilities); const allAllowlisted = ids.length > 0 && ids.every((id) => allowlist[id]); if (allAllowlisted) continue; From fb3611cb669033ffe7190974625a4bc5c3fa4bca Mon Sep 17 00:00:00 2001 From: majormaxx Date: Sun, 27 Sep 2026 20:12:12 +0100 Subject: [PATCH 8/8] cleared eslint errors across DTOs, interceptors and specs --- src/auth/dto/register.dto.ts | 4 +- src/common/cache/memory-cache.store.ts | 4 +- src/common/filters/domain-exception.filter.ts | 3 +- .../bigint-serializer.interceptor.spec.ts | 146 +++++++----------- .../bigint-serializer.interceptor.ts | 27 ++-- .../idempotency.interceptor.spec.ts | 4 +- src/common/utils/currency.ts | 1 - src/events/dto/create-event.dto.ts | 8 +- src/events/dto/create-ticket-type.dto.ts | 4 +- src/gates/dto/create-gate.dto.ts | 4 +- .../dto/create-organization.dto.ts | 4 +- .../organizations.service.spec.ts | 2 +- .../pending-tx-cleanup.service.spec.ts | 4 +- src/pending-tx/pending-tx.service.spec.ts | 10 +- .../dto/register-scanner-device.dto.ts | 4 +- .../scanner-devices.service.spec.ts | 2 +- src/tickets/dto/confirm-check-in.dto.ts | 4 +- src/tickets/tickets.service.ts | 8 +- src/waitlist/waitlist.service.spec.ts | 2 +- src/webhooks/bullmq-webhook-queue.spec.ts | 4 +- .../dto/create-webhook-endpoint.dto.ts | 8 +- src/webhooks/webhooks.module.spec.ts | 4 +- src/webhooks/webhooks.service.spec.ts | 9 +- 23 files changed, 139 insertions(+), 131 deletions(-) diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts index 730369c..73b6b25 100644 --- a/src/auth/dto/register.dto.ts +++ b/src/auth/dto/register.dto.ts @@ -9,7 +9,9 @@ export class RegisterDto { @MinLength(10) password!: string; - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(1) @MaxLength(256) diff --git a/src/common/cache/memory-cache.store.ts b/src/common/cache/memory-cache.store.ts index 9aa43c0..2159345 100644 --- a/src/common/cache/memory-cache.store.ts +++ b/src/common/cache/memory-cache.store.ts @@ -36,7 +36,9 @@ export class MemoryCacheStore implements CacheStore { json = serializeCacheValue(value); } catch (err) { // Reject rather than throw, so callers see the same async contract as Redis. - return Promise.reject(err); + return Promise.reject( + err instanceof Error ? err : new Error(String(err)), + ); } // Re-insert so a refreshed key counts as the newest for eviction. diff --git a/src/common/filters/domain-exception.filter.ts b/src/common/filters/domain-exception.filter.ts index 4f6892e..e17addf 100644 --- a/src/common/filters/domain-exception.filter.ts +++ b/src/common/filters/domain-exception.filter.ts @@ -4,6 +4,7 @@ import { ExceptionFilter, HttpStatus, } from '@nestjs/common'; +import type { Response } from 'express'; import { DomainError } from '../errors/domain.error'; const STATUS_BY_CODE: Readonly> = { @@ -14,7 +15,7 @@ const STATUS_BY_CODE: Readonly> = { @Catch(DomainError) export class DomainExceptionFilter implements ExceptionFilter { catch(exception: DomainError, host: ArgumentsHost): void { - const response = host.switchToHttp().getResponse(); + const response = host.switchToHttp().getResponse(); const statusCode = STATUS_BY_CODE[exception.code] ?? HttpStatus.INTERNAL_SERVER_ERROR; diff --git a/src/common/interceptors/bigint-serializer.interceptor.spec.ts b/src/common/interceptors/bigint-serializer.interceptor.spec.ts index d8de23f..e97f15b 100644 --- a/src/common/interceptors/bigint-serializer.interceptor.spec.ts +++ b/src/common/interceptors/bigint-serializer.interceptor.spec.ts @@ -1,105 +1,73 @@ -import { ExecutionContext, CallHandler } from '@nestjs/common'; -import { of } from 'rxjs'; +import type { ExecutionContext } from '@nestjs/common'; +import { firstValueFrom, of } from 'rxjs'; import { BigIntSerializerInterceptor } from './bigint-serializer.interceptor'; describe('BigIntSerializerInterceptor', () => { - let interceptor: BigIntSerializerInterceptor; - let mockExecutionContext: ExecutionContext; - let mockCallHandler: CallHandler; + const interceptor = new BigIntSerializerInterceptor(); - beforeEach(() => { - interceptor = new BigIntSerializerInterceptor(); - mockExecutionContext = {} as ExecutionContext; - }); - - it('should serialize bigint to string', (done) => { - mockCallHandler = { - handle: () => of({ value: BigInt('12345678901234567890') }), - }; + /** Runs `payload` through the interceptor and returns the serialized body. */ + async function serialize>( + payload: unknown, + ): Promise { + const result = await firstValueFrom( + interceptor.intercept({} as ExecutionContext, { + handle: () => of(payload), + }), + ); + return result as T; + } - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { - expect(result.value).toBe('12345678901234567890'); - expect(typeof result.value).toBe('string'); - done(); - }); + it('should serialize bigint to string', async () => { + const result = await serialize({ value: BigInt('12345678901234567890') }); + expect(result.value).toBe('12345678901234567890'); + expect(typeof result.value).toBe('string'); }); - it('should serialize nested bigints', (done) => { - mockCallHandler = { - handle: () => - of({ - price: BigInt('1000000'), - ticket: { chainTicketId: BigInt('999999') }, - }), - }; - - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { - expect(result.price).toBe('1000000'); - expect(result.ticket.chainTicketId).toBe('999999'); - done(); - }); + it('should serialize nested bigints', async () => { + const result = await serialize<{ + price: string; + ticket: { chainTicketId: string }; + }>({ + price: BigInt('1000000'), + ticket: { chainTicketId: BigInt('999999') }, + }); + expect(result.price).toBe('1000000'); + expect(result.ticket.chainTicketId).toBe('999999'); }); - it('should serialize bigints in arrays', (done) => { - mockCallHandler = { - handle: () => - of([{ id: BigInt('1') }, { id: BigInt('2') }, { id: BigInt('3') }]), - }; - - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('2'); - expect(result[2].id).toBe('3'); - done(); - }); + it('should serialize bigints in arrays', async () => { + const result = await serialize>([ + { id: BigInt('1') }, + { id: BigInt('2') }, + { id: BigInt('3') }, + ]); + expect(result[0].id).toBe('1'); + expect(result[1].id).toBe('2'); + expect(result[2].id).toBe('3'); }); - it('should handle null and undefined', (done) => { - mockCallHandler = { - handle: () => - of({ - nullValue: null, - undefinedValue: undefined, - normalValue: 'test', - }), - }; - - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { - expect(result.nullValue).toBeNull(); - expect(result.undefinedValue).toBeUndefined(); - expect(result.normalValue).toBe('test'); - done(); - }); + it('should handle null and undefined', async () => { + const result = await serialize({ + nullValue: null, + undefinedValue: undefined, + normalValue: 'test', + }); + expect(result.nullValue).toBeNull(); + expect(result.undefinedValue).toBeUndefined(); + expect(result.normalValue).toBe('test'); }); - it('should preserve other data types', (done) => { - mockCallHandler = { - handle: () => - of({ - string: 'hello', - number: 42, - boolean: true, - date: new Date('2026-09-24'), - }), - }; - - interceptor - .intercept(mockExecutionContext, mockCallHandler) - .subscribe((result) => { - expect(result.string).toBe('hello'); - expect(result.number).toBe(42); - expect(result.boolean).toBe(true); - expect(result.date instanceof Date).toBe(true); - done(); - }); + it('should preserve other data types', async () => { + const result = await serialize({ + string: 'hello', + number: 42, + boolean: true, + date: new Date('2026-09-24'), + }); + expect(result.string).toBe('hello'); + expect(result.number).toBe(42); + expect(result.boolean).toBe(true); + expect(result.date instanceof Date).toBe(true); }); it('should support JSON.stringify on objects containing BigInt', () => { diff --git a/src/common/interceptors/bigint-serializer.interceptor.ts b/src/common/interceptors/bigint-serializer.interceptor.ts index 422bcde..5f1dfba 100644 --- a/src/common/interceptors/bigint-serializer.interceptor.ts +++ b/src/common/interceptors/bigint-serializer.interceptor.ts @@ -7,11 +7,9 @@ import { import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -if ( - typeof BigInt.prototype !== 'undefined' && - !(BigInt.prototype as any).toJSON -) { - (BigInt.prototype as any).toJSON = function (this: bigint) { +const bigIntPrototype = BigInt.prototype as { toJSON?: () => string }; +if (!bigIntPrototype.toJSON) { + bigIntPrototype.toJSON = function (this: bigint) { return this.toString(); }; } @@ -22,11 +20,16 @@ if ( */ @Injectable() export class BigIntSerializerInterceptor implements NestInterceptor { - intercept(_context: ExecutionContext, next: CallHandler): Observable { - return next.handle().pipe(map((data) => this.serializeBigInts(data))); + intercept( + _context: ExecutionContext, + next: CallHandler, + ): Observable { + return next + .handle() + .pipe(map((data: unknown) => this.serializeBigInts(data))); } - private serializeBigInts(value: any): any { + private serializeBigInts(value: unknown): unknown { if (value === null || value === undefined) { return value; } @@ -44,11 +47,9 @@ export class BigIntSerializerInterceptor implements NestInterceptor { } if (typeof value === 'object') { - const converted: any = {}; - for (const key in value) { - if (Object.prototype.hasOwnProperty.call(value, key)) { - converted[key] = this.serializeBigInts(value[key]); - } + const converted: Record = {}; + for (const [key, nested] of Object.entries(value)) { + converted[key] = this.serializeBigInts(nested); } return converted; } diff --git a/src/common/interceptors/idempotency.interceptor.spec.ts b/src/common/interceptors/idempotency.interceptor.spec.ts index 6254351..bde82c3 100644 --- a/src/common/interceptors/idempotency.interceptor.spec.ts +++ b/src/common/interceptors/idempotency.interceptor.spec.ts @@ -1,4 +1,4 @@ -import type { CallHandler, ExecutionContext } from '@nestjs/common'; +import type { ExecutionContext } from '@nestjs/common'; import { of } from 'rxjs'; import { IdempotencyInterceptor } from './idempotency.interceptor'; import type { ConfigService } from '@nestjs/config'; @@ -13,7 +13,7 @@ function buildContext(headers: Record) { } as unknown as ExecutionContext; } -function buildHandler(response: unknown): CallHandler { +function buildHandler(response: unknown): { handle: jest.Mock } { return { handle: jest.fn().mockReturnValue(of(response)) }; } diff --git a/src/common/utils/currency.ts b/src/common/utils/currency.ts index d7a7797..22ceb62 100644 --- a/src/common/utils/currency.ts +++ b/src/common/utils/currency.ts @@ -15,7 +15,6 @@ export function formatPrice( throw new Error('Decimals cannot be negative'); } - const priceStr = priceSmallestUnit.toString(); const divisor = 10n ** BigInt(decimals); const integerPart = priceSmallestUnit / divisor; const fractionalPart = priceSmallestUnit % divisor; diff --git a/src/events/dto/create-event.dto.ts b/src/events/dto/create-event.dto.ts index ff5b068..846f7de 100644 --- a/src/events/dto/create-event.dto.ts +++ b/src/events/dto/create-event.dto.ts @@ -21,7 +21,9 @@ import { Industry } from '@prisma/client'; export const STARTS_AT_PAST_TOLERANCE_MS = 60_000; export class CreateEventDto { - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(2) @MaxLength(256) @@ -32,7 +34,9 @@ export class CreateEventDto { }) category!: Industry; - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(1) @MaxLength(256) diff --git a/src/events/dto/create-ticket-type.dto.ts b/src/events/dto/create-ticket-type.dto.ts index 82af13b..6e0b944 100644 --- a/src/events/dto/create-ticket-type.dto.ts +++ b/src/events/dto/create-ticket-type.dto.ts @@ -11,7 +11,9 @@ import { import { IsBigIntString } from '../../common/decorators/is-bigint-string.decorator'; export class CreateTicketTypeDto { - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(1) @MaxLength(256) diff --git a/src/gates/dto/create-gate.dto.ts b/src/gates/dto/create-gate.dto.ts index bc3a702..e901d38 100644 --- a/src/gates/dto/create-gate.dto.ts +++ b/src/gates/dto/create-gate.dto.ts @@ -2,7 +2,9 @@ import { Transform } from 'class-transformer'; import { IsString, MaxLength, MinLength } from 'class-validator'; export class CreateGateDto { - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(1) @MaxLength(256) diff --git a/src/organizations/dto/create-organization.dto.ts b/src/organizations/dto/create-organization.dto.ts index 8a73049..8fda760 100644 --- a/src/organizations/dto/create-organization.dto.ts +++ b/src/organizations/dto/create-organization.dto.ts @@ -12,7 +12,9 @@ import { Industry } from '@prisma/client'; import { IsStellarPublicKey } from '../../common/decorators/is-stellar-public-key.decorator'; export class CreateOrganizationDto { - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(2) @MaxLength(256) diff --git a/src/organizations/organizations.service.spec.ts b/src/organizations/organizations.service.spec.ts index 6cbfa78..6733095 100644 --- a/src/organizations/organizations.service.spec.ts +++ b/src/organizations/organizations.service.spec.ts @@ -116,7 +116,7 @@ describe('OrganizationsService', () => { expect(prisma.organization.update).toHaveBeenCalledWith({ where: { id: 'org-1' }, - data: { deletedAt: expect.any(Date) }, + data: { deletedAt: expect.any(Date) as Date }, }); }); diff --git a/src/pending-tx/pending-tx-cleanup.service.spec.ts b/src/pending-tx/pending-tx-cleanup.service.spec.ts index 5148d3e..c5cb5ca 100644 --- a/src/pending-tx/pending-tx-cleanup.service.spec.ts +++ b/src/pending-tx/pending-tx-cleanup.service.spec.ts @@ -31,7 +31,9 @@ describe('PendingTxCleanupService', () => { expect(pendingTx.deleteExpired).toHaveBeenCalledTimes(1); expect(scheduler.addInterval).toHaveBeenCalledTimes(1); - expect(scheduler.addInterval.mock.calls[0][0]).toBe('pending-tx-cleanup'); + expect((scheduler.addInterval.mock.calls[0] as unknown[])[0]).toBe( + 'pending-tx-cleanup', + ); }); it('uses the configured cleanup interval', () => { diff --git a/src/pending-tx/pending-tx.service.spec.ts b/src/pending-tx/pending-tx.service.spec.ts index e850c5f..f1badba 100644 --- a/src/pending-tx/pending-tx.service.spec.ts +++ b/src/pending-tx/pending-tx.service.spec.ts @@ -2,6 +2,10 @@ import { PendingTxService } from './pending-tx.service'; import type { PrismaService } from '../prisma/prisma.service'; import type { ConfigService } from '@nestjs/config'; +type CreateArgs = { + data: { type: string; userId: string; payload: unknown; expiresAt: Date }; +}; + describe('PendingTxService', () => { let service: PendingTxService; let prisma: { @@ -29,7 +33,7 @@ describe('PendingTxService', () => { await service.record('purchase', 'user-1', { ticketTypeId: 'tt-1' }); expect(prisma.pendingTx.create).toHaveBeenCalledTimes(1); - const { data } = prisma.pendingTx.create.mock.calls[0][0]; + const { data } = (prisma.pendingTx.create.mock.calls[0] as [CreateArgs])[0]; expect(data.type).toBe('purchase'); expect(data.userId).toBe('user-1'); expect(data.payload).toEqual({ ticketTypeId: 'tt-1' }); @@ -41,7 +45,7 @@ describe('PendingTxService', () => { const before = Date.now(); await service.record('issue', 'user-1', {}); - const { data } = prisma.pendingTx.create.mock.calls[0][0]; + const { data } = (prisma.pendingTx.create.mock.calls[0] as [CreateArgs])[0]; expect(data.expiresAt.getTime()).toBeLessThanOrEqual( before + 5 * 60_000 + 1_000, ); @@ -51,7 +55,7 @@ describe('PendingTxService', () => { const count = await service.deleteExpired(); expect(prisma.pendingTx.deleteMany).toHaveBeenCalledWith({ - where: { expiresAt: { lt: expect.any(Date) } }, + where: { expiresAt: { lt: expect.any(Date) as Date } }, }); expect(count).toBe(3); }); diff --git a/src/scanner-devices/dto/register-scanner-device.dto.ts b/src/scanner-devices/dto/register-scanner-device.dto.ts index 144e791..f07952a 100644 --- a/src/scanner-devices/dto/register-scanner-device.dto.ts +++ b/src/scanner-devices/dto/register-scanner-device.dto.ts @@ -2,7 +2,9 @@ import { Transform } from 'class-transformer'; import { IsString, MaxLength, MinLength } from 'class-validator'; export class RegisterScannerDeviceDto { - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MinLength(1) @MaxLength(256) diff --git a/src/scanner-devices/scanner-devices.service.spec.ts b/src/scanner-devices/scanner-devices.service.spec.ts index 3705795..991d99b 100644 --- a/src/scanner-devices/scanner-devices.service.spec.ts +++ b/src/scanner-devices/scanner-devices.service.spec.ts @@ -99,7 +99,7 @@ describe('ScannerDevicesService', () => { expect(prisma.scannerDevice.update).toHaveBeenCalledWith({ where: { id: 'device-1' }, - data: { revokedAt: expect.any(Date) }, + data: { revokedAt: expect.any(Date) as Date }, }); }); }); diff --git a/src/tickets/dto/confirm-check-in.dto.ts b/src/tickets/dto/confirm-check-in.dto.ts index a2fe6c7..6886506 100644 --- a/src/tickets/dto/confirm-check-in.dto.ts +++ b/src/tickets/dto/confirm-check-in.dto.ts @@ -10,7 +10,9 @@ export class ConfirmCheckInDto extends ConfirmSignedTxDto { /** Reason for check-in when scanner fails and staff override is used. */ @IsOptional() - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MaxLength(512) reason?: string; diff --git a/src/tickets/tickets.service.ts b/src/tickets/tickets.service.ts index 4ffd1a6..8ee31aa 100644 --- a/src/tickets/tickets.service.ts +++ b/src/tickets/tickets.service.ts @@ -916,7 +916,9 @@ export class TicketsService { const target = (err.meta as { target?: unknown } | undefined)?.target; const targets = Array.isArray(target) ? target.join(',') - : String(target ?? ''); + : typeof target === 'string' + ? target + : ''; if (targets.includes('seat') || targets.includes('Ticket_eventId_seat')) { throw new ConflictException( 'That seat has already been issued for this event', @@ -952,7 +954,9 @@ export class TicketsService { const target = (err.meta as { target?: unknown } | undefined)?.target; const targets = Array.isArray(target) ? target.join(',') - : String(target ?? ''); + : typeof target === 'string' + ? target + : ''; if (targets.includes('ResaleListing_ticketId_active')) { throw new ConflictException( 'This ticket already has an active resale listing', diff --git a/src/waitlist/waitlist.service.spec.ts b/src/waitlist/waitlist.service.spec.ts index 1786c34..1f8b507 100644 --- a/src/waitlist/waitlist.service.spec.ts +++ b/src/waitlist/waitlist.service.spec.ts @@ -182,7 +182,7 @@ describe('WaitlistService', () => { where: { id: { in: ['entry-1', 'entry-2'] } }, data: expect.objectContaining({ status: WaitlistEntryStatus.OFFERED, - }), + }) as object, }), ); }); diff --git a/src/webhooks/bullmq-webhook-queue.spec.ts b/src/webhooks/bullmq-webhook-queue.spec.ts index a6829d2..edf5e78 100644 --- a/src/webhooks/bullmq-webhook-queue.spec.ts +++ b/src/webhooks/bullmq-webhook-queue.spec.ts @@ -52,7 +52,7 @@ describe('BullMqWebhookQueue', () => { defaultJobOptions: expect.objectContaining({ attempts: 4, backoff: { type: 'exponential', delay: 2_000 }, - }), + }) as object, }), ); }); @@ -79,7 +79,7 @@ describe('BullMqWebhookQueue', () => { const { bullmq, Worker } = fakeBullMq(); BullMqWebhookQueue.create(bullmq, options); - const processor = Worker.mock.calls[0][1] as (job: { + const processor = (Worker.mock.calls[0] as unknown[])[1] as (job: { data: typeof delivery; }) => Promise; await processor({ data: delivery }); diff --git a/src/webhooks/dto/create-webhook-endpoint.dto.ts b/src/webhooks/dto/create-webhook-endpoint.dto.ts index 122ce3d..84ce3a4 100644 --- a/src/webhooks/dto/create-webhook-endpoint.dto.ts +++ b/src/webhooks/dto/create-webhook-endpoint.dto.ts @@ -14,13 +14,17 @@ export class CreateWebhookEndpointDto { url!: string; @IsOptional() - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MaxLength(512) events?: string; @IsOptional() - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + @Transform(({ value }: { value: unknown }) => + typeof value === 'string' ? value.trim() : value, + ) @IsString() @MaxLength(256) secret?: string; diff --git a/src/webhooks/webhooks.module.spec.ts b/src/webhooks/webhooks.module.spec.ts index 044d45c..0e62d84 100644 --- a/src/webhooks/webhooks.module.spec.ts +++ b/src/webhooks/webhooks.module.spec.ts @@ -80,7 +80,7 @@ describe('createWebhookQueue', () => { defaultJobOptions: expect.objectContaining({ attempts: 5, backoff: { type: 'exponential', delay: 5_000 }, - }), + }) as object, }), ); }); @@ -104,7 +104,7 @@ describe('createWebhookQueue', () => { defaultJobOptions: expect.objectContaining({ attempts: 8, backoff: { type: 'exponential', delay: 250 }, - }), + }) as object, }), ); }); diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts index 59fd524..92f352e 100644 --- a/src/webhooks/webhooks.service.spec.ts +++ b/src/webhooks/webhooks.service.spec.ts @@ -7,7 +7,14 @@ import { WEBHOOK_QUEUE } from './webhook-queue'; describe('WebhooksService', () => { let service: WebhooksService; - let prisma: { webhookEndpoint: any }; + let prisma: { + webhookEndpoint: { + create: jest.Mock; + findMany: jest.Mock; + findFirst: jest.Mock; + delete: jest.Mock; + }; + }; let organizationsService: { assertMember: jest.Mock }; let webhookQueue: { enqueue: jest.Mock };