FRE-4517, FRE-4499: Complete SpamShield implementation and billing updates
- SpamFeedback table migration with timestamp index - Real-time interception engine completion - Billing service enhancements - Classifier and rule engine updates Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -2,6 +2,7 @@ import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import helmet from "@fastify/helmet";
|
||||
import sensible from "@fastify/sensible";
|
||||
import { extractOrGenerateRequestId } from "@shieldai/types";
|
||||
import { darkwatchRoutes, voiceprintRoutes } from "./routes";
|
||||
|
||||
const app = Fastify({
|
||||
@@ -15,6 +16,15 @@ async function bootstrap() {
|
||||
await app.register(helmet);
|
||||
await app.register(sensible);
|
||||
|
||||
app.addHook("onRequest", async (request, _reply) => {
|
||||
const requestId = extractOrGenerateRequestId(request.headers);
|
||||
request.id = requestId;
|
||||
const pinoLog = request.log as typeof request.log & { bindings?: Record<string, string>; bindActive?: () => void };
|
||||
pinoLog.bindings = { requestId };
|
||||
pinoLog.bindActive?.();
|
||||
request.headers["x-request-id"] = requestId;
|
||||
});
|
||||
|
||||
await app.register(darkwatchRoutes);
|
||||
await app.register(voiceprintRoutes);
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"stripe": "^15.0.0",
|
||||
"zod": "^3.22.0",
|
||||
"express": "^4.18.0"
|
||||
"express": "^4.22.1",
|
||||
"stripe": "^14.25.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.0",
|
||||
|
||||
712
packages/shared-billing/pnpm-lock.yaml
generated
Normal file
712
packages/shared-billing/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,712 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
express:
|
||||
specifier: ^4.22.1
|
||||
version: 4.22.1
|
||||
stripe:
|
||||
specifier: ^14.25.0
|
||||
version: 14.25.0
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^4.17.0
|
||||
version: 4.17.25
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/express-serve-static-core@4.19.8':
|
||||
resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==}
|
||||
|
||||
'@types/express@4.17.25':
|
||||
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
|
||||
|
||||
'@types/http-errors@2.0.5':
|
||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/node@25.6.0':
|
||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||
|
||||
'@types/qs@6.15.0':
|
||||
resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==}
|
||||
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/send@0.17.6':
|
||||
resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||
|
||||
'@types/serve-static@1.15.10':
|
||||
resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
|
||||
|
||||
accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
array-flatten@1.1.1:
|
||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
|
||||
body-parser@1.20.5:
|
||||
resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bound@1.0.4:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
content-disposition@0.5.4:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
content-type@1.0.5:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cookie-signature@1.0.7:
|
||||
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||
|
||||
cookie@0.7.2:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
destroy@1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
encodeurl@2.0.0:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
etag@1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
express@4.22.1:
|
||||
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
finalhandler@1.3.2:
|
||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fresh@0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.3:
|
||||
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
media-typer@0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
merge-descriptors@1.0.3:
|
||||
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
||||
|
||||
methods@1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime@1.6.0:
|
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
negotiator@0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-to-regexp@0.1.13:
|
||||
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
qs@6.14.2:
|
||||
resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
qs@6.15.1:
|
||||
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
raw-body@2.5.3:
|
||||
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
send@0.19.2:
|
||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
serve-static@1.16.3:
|
||||
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
side-channel-list@1.0.1:
|
||||
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.1.0:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
stripe@14.25.0:
|
||||
resolution: {integrity: sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==}
|
||||
engines: {node: '>=12.*'}
|
||||
|
||||
toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@7.19.2:
|
||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||
|
||||
unpipe@1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/express-serve-static-core@4.19.8':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
'@types/qs': 6.15.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
|
||||
'@types/express@4.17.25':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.6
|
||||
'@types/express-serve-static-core': 4.19.8
|
||||
'@types/qs': 6.15.0
|
||||
'@types/serve-static': 1.15.10
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/node@25.6.0':
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@types/qs@6.15.0': {}
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/send@0.17.6':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/serve-static@1.15.10':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.6.0
|
||||
'@types/send': 0.17.6
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
negotiator: 0.6.3
|
||||
|
||||
array-flatten@1.1.1: {}
|
||||
|
||||
body-parser@1.20.5:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.4.24
|
||||
on-finished: 2.4.1
|
||||
qs: 6.15.1
|
||||
raw-body: 2.5.3
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
call-bound@1.0.4:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
content-disposition@0.5.4:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
cookie-signature@1.0.7: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
destroy@1.2.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
express@4.22.1:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
body-parser: 1.20.5
|
||||
content-disposition: 0.5.4
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.7
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 1.3.2
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.1
|
||||
merge-descriptors: 1.0.3
|
||||
methods: 1.1.2
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 0.1.13
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.14.2
|
||||
range-parser: 1.2.1
|
||||
safe-buffer: 5.2.1
|
||||
send: 0.19.2
|
||||
serve-static: 1.16.3
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.2
|
||||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
finalhandler@1.3.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
statuses: 2.0.2
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
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.3
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
hasown@2.0.3:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.2
|
||||
toidentifier: 1.0.1
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
merge-descriptors@1.0.3: {}
|
||||
|
||||
methods@1.1.2: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime@1.6.0: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-to-regexp@0.1.13: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
qs@6.14.2:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
qs@6.15.1:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@2.5.3:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
send@0.19.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.1
|
||||
mime: 1.6.0
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@1.16.3:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.19.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
side-channel-list@1.0.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
side-channel@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-list: 1.0.1
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
stripe@14.25.0:
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
qs: 6.15.1
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
type-is@1.6.18:
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@7.19.2: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
zod@3.25.76: {}
|
||||
@@ -52,43 +52,47 @@ export const BillingConfigSchema = z.object({
|
||||
|
||||
export type BillingConfig = z.infer<typeof BillingConfigSchema>;
|
||||
|
||||
export const loadBillingConfig = (): BillingConfig => ({
|
||||
stripe: {
|
||||
apiKey: process.env.STRIPE_API_KEY!,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
pricingTableId: process.env.STRIPE_PRICING_TABLE_ID,
|
||||
},
|
||||
tiers: {
|
||||
free: {
|
||||
priceId: process.env.STRIPE_FREE_TIER_PRICE_ID || 'price_free',
|
||||
monthlyPriceCents: 0,
|
||||
callMinutesLimit: 100,
|
||||
smsCountLimit: 500,
|
||||
darkWebScans: 1,
|
||||
export const loadBillingConfig = (): BillingConfig => {
|
||||
const rawConfig = {
|
||||
stripe: {
|
||||
apiKey: process.env.STRIPE_API_KEY!,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
pricingTableId: process.env.STRIPE_PRICING_TABLE_ID,
|
||||
},
|
||||
basic: {
|
||||
priceId: process.env.STRIPE_BASIC_TIER_PRICE_ID || 'price_basic',
|
||||
monthlyPriceCents: 999,
|
||||
callMinutesLimit: 500,
|
||||
smsCountLimit: 2000,
|
||||
darkWebScans: 12,
|
||||
tiers: {
|
||||
free: {
|
||||
priceId: process.env.STRIPE_FREE_TIER_PRICE_ID || 'price_free',
|
||||
monthlyPriceCents: 0,
|
||||
callMinutesLimit: 100,
|
||||
smsCountLimit: 500,
|
||||
darkWebScans: 1,
|
||||
},
|
||||
basic: {
|
||||
priceId: process.env.STRIPE_BASIC_TIER_PRICE_ID || 'price_basic',
|
||||
monthlyPriceCents: 999,
|
||||
callMinutesLimit: 500,
|
||||
smsCountLimit: 2000,
|
||||
darkWebScans: 12,
|
||||
},
|
||||
plus: {
|
||||
priceId: process.env.STRIPE_PLUS_TIER_PRICE_ID || 'price_plus',
|
||||
monthlyPriceCents: 1999,
|
||||
callMinutesLimit: 2000,
|
||||
smsCountLimit: 10000,
|
||||
darkWebScans: 12,
|
||||
voiceCloning: true,
|
||||
},
|
||||
premium: {
|
||||
priceId: process.env.STRIPE_PREMIUM_TIER_PRICE_ID || 'price_premium',
|
||||
monthlyPriceCents: 4999,
|
||||
callMinutesLimit: 10000,
|
||||
smsCountLimit: 50000,
|
||||
darkWebScans: 12,
|
||||
voiceCloning: true,
|
||||
homeTitleMonitor: true,
|
||||
},
|
||||
},
|
||||
plus: {
|
||||
priceId: process.env.STRIPE_PLUS_TIER_PRICE_ID || 'price_plus',
|
||||
monthlyPriceCents: 1999,
|
||||
callMinutesLimit: 2000,
|
||||
smsCountLimit: 10000,
|
||||
darkWebScans: 12,
|
||||
voiceCloning: true,
|
||||
},
|
||||
premium: {
|
||||
priceId: process.env.STRIPE_PREMIUM_TIER_PRICE_ID || 'price_premium',
|
||||
monthlyPriceCents: 4999,
|
||||
callMinutesLimit: 10000,
|
||||
smsCountLimit: 50000,
|
||||
darkWebScans: 12,
|
||||
voiceCloning: true,
|
||||
homeTitleMonitor: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return BillingConfigSchema.parse(rawConfig);
|
||||
};
|
||||
|
||||
@@ -19,23 +19,39 @@ export function requireTier(
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const userTier = req.tier;
|
||||
const { userId } = req;
|
||||
|
||||
if (!userTier) {
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Authentication required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowedTiers.includes(userTier)) {
|
||||
res.status(403).json({
|
||||
error: 'Tier not authorized',
|
||||
required: allowedTiers,
|
||||
current: userTier,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const userTier = await billingService.getUserTier(userId);
|
||||
|
||||
next();
|
||||
if (!userTier) {
|
||||
res.status(401).json({ error: 'User tier not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
req.tier = userTier;
|
||||
|
||||
if (!allowedTiers.includes(userTier)) {
|
||||
res.status(403).json({
|
||||
error: 'Tier not authorized',
|
||||
required: allowedTiers,
|
||||
current: userTier,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
error: 'Failed to verify tier',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,12 +139,10 @@ export function withSubscription() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch subscription from database
|
||||
// TODO: Replace with actual database query
|
||||
const subscriptionId = (req as any).subscriptionId;
|
||||
const subscription = await billingService.getUserSubscription(userId);
|
||||
|
||||
if (subscriptionId) {
|
||||
req.subscriptionId = subscriptionId;
|
||||
if (subscription) {
|
||||
req.subscriptionId = subscription.id;
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
@@ -3,7 +3,19 @@ import { loadBillingConfig, SubscriptionTier } from '../config/billing.config';
|
||||
import type { Subscription, SubscriptionCreateSchema, SubscriptionUpdateSchema } from '../models/subscription.model';
|
||||
|
||||
const config = loadBillingConfig();
|
||||
const stripe = new Stripe(config.stripe.apiKey, { apiVersion: '2024-04-10' });
|
||||
const stripe = new Stripe(config.stripe.apiKey, { apiVersion: '2023-10-16' });
|
||||
|
||||
const processedEvents = new Map<string, number>();
|
||||
const IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function cleanupOldEvents(): void {
|
||||
const now = Date.now();
|
||||
for (const [eventId, timestamp] of processedEvents.entries()) {
|
||||
if (now - timestamp > IDEMPOTENCY_TTL_MS) {
|
||||
processedEvents.delete(eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BillingService {
|
||||
private static instance: BillingService;
|
||||
@@ -34,6 +46,51 @@ export class BillingService {
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyCustomerOwnership(
|
||||
customerId: string,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const customer = await stripe.customers.retrieve(customerId);
|
||||
const customerUserId = (customer as Stripe.Customer).metadata?.userId;
|
||||
|
||||
if (customerUserId !== userId) {
|
||||
throw new Error(
|
||||
`Customer ${customerId} does not belong to user ${userId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getUserTier(userId: string): Promise<SubscriptionTier | null> {
|
||||
try {
|
||||
const customers = await stripe.customers.list({
|
||||
limit: 100,
|
||||
expand: ['data.subscriptions'],
|
||||
});
|
||||
|
||||
const customer = customers.data.find(
|
||||
(c: Stripe.Customer) =>
|
||||
c.metadata?.userId === userId && c.subscriptions?.data.length && c.subscriptions.data.length > 0
|
||||
);
|
||||
|
||||
if (!customer || !customer.subscriptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeSubscription = customer.subscriptions.data.find(
|
||||
(sub: Stripe.Subscription) => sub.status === 'active'
|
||||
);
|
||||
|
||||
if (!activeSubscription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tier = activeSubscription.metadata?.tier as SubscriptionTier;
|
||||
return tier || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createSubscription(
|
||||
userId: string,
|
||||
tier: SubscriptionTier,
|
||||
@@ -41,21 +98,36 @@ export class BillingService {
|
||||
): Promise<{ subscription: Stripe.Subscription; customer: Stripe.Customer }> {
|
||||
const tierConfig = config.tiers[tier];
|
||||
|
||||
const customer = await this.getCustomer(customerId);
|
||||
if (!customer) {
|
||||
throw new Error(`Customer ${customerId} not found`);
|
||||
}
|
||||
|
||||
await this.verifyCustomerOwnership(customerId, userId);
|
||||
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
customer: customerId,
|
||||
items: [{ price: tierConfig.priceId }],
|
||||
metadata: { userId, tier },
|
||||
});
|
||||
|
||||
const customer = await this.getCustomer(customerId);
|
||||
|
||||
return { subscription, customer: customer! };
|
||||
return { subscription, customer };
|
||||
}
|
||||
|
||||
async cancelSubscription(
|
||||
subscriptionId: string,
|
||||
userId: string,
|
||||
cancelAtPeriodEnd: boolean = false
|
||||
): Promise<Stripe.Subscription> {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
const customerUserId = subscription.metadata?.userId;
|
||||
|
||||
if (customerUserId !== userId) {
|
||||
throw new Error(
|
||||
`Subscription ${subscriptionId} does not belong to user ${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (cancelAtPeriodEnd) {
|
||||
return await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at_period_end: true,
|
||||
@@ -66,11 +138,19 @@ export class BillingService {
|
||||
|
||||
async updateSubscription(
|
||||
subscriptionId: string,
|
||||
userId: string,
|
||||
newTier: SubscriptionTier
|
||||
): Promise<Stripe.Subscription> {
|
||||
const newTierConfig = config.tiers[newTier];
|
||||
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
const customerUserId = subscription.metadata?.userId;
|
||||
|
||||
if (customerUserId !== userId) {
|
||||
throw new Error(
|
||||
`Subscription ${subscriptionId} does not belong to user ${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
const newTierConfig = config.tiers[newTier];
|
||||
|
||||
const updated = await stripe.subscriptions.update(subscriptionId, {
|
||||
proration_behavior: 'create_prorations',
|
||||
@@ -104,6 +184,29 @@ export class BillingService {
|
||||
}
|
||||
}
|
||||
|
||||
async getUserSubscription(userId: string): Promise<Stripe.Subscription | null> {
|
||||
try {
|
||||
const customers = await stripe.customers.list({
|
||||
limit: 100,
|
||||
expand: ['data.subscriptions'],
|
||||
});
|
||||
|
||||
for (const customer of customers.data) {
|
||||
if (customer.metadata?.userId === userId && customer.subscriptions) {
|
||||
const activeSub = customer.subscriptions.data.find(
|
||||
(sub: Stripe.Subscription) => sub.status === 'active'
|
||||
);
|
||||
if (activeSub) {
|
||||
return activeSub;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getTierLimits(tier: SubscriptionTier) {
|
||||
return config.tiers[tier];
|
||||
}
|
||||
@@ -130,24 +233,41 @@ export class BillingService {
|
||||
description: string,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<Stripe.Invoice> {
|
||||
return await stripe.invoices.create({
|
||||
const invoice = await stripe.invoices.create({
|
||||
customer: customerId,
|
||||
line_items: [
|
||||
{
|
||||
amount_data: { currency: 'usd', unit_amount: amount },
|
||||
description: description,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
metadata: metadata,
|
||||
});
|
||||
|
||||
await stripe.invoiceItems.create({
|
||||
invoice: invoice.id,
|
||||
customer: customerId,
|
||||
price_data: {
|
||||
currency: 'usd',
|
||||
unit_amount: amount,
|
||||
product: 'default_product',
|
||||
},
|
||||
description: description,
|
||||
quantity: 1,
|
||||
});
|
||||
|
||||
return await stripe.invoices.retrieve(invoice.id);
|
||||
}
|
||||
|
||||
async handleWebhook(
|
||||
sig: string,
|
||||
body: Buffer
|
||||
): Promise<Stripe.Event> {
|
||||
return stripe.webhooks.constructEvent(body, sig, config.stripe.webhookSecret);
|
||||
): Promise<Stripe.Event | null> {
|
||||
const event = stripe.webhooks.constructEvent(body, sig, config.stripe.webhookSecret);
|
||||
|
||||
cleanupOldEvents();
|
||||
|
||||
if (processedEvents.has(event.id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
processedEvents.set(event.id, Date.now());
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
async getInvoiceHistory(customerId: string): Promise<Stripe.ApiList<Stripe.Invoice>> {
|
||||
|
||||
5160
packages/shared-notifications/package-lock.json
generated
Normal file
5160
packages/shared-notifications/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -176,3 +176,5 @@ export interface SchedulerConfig {
|
||||
intervalMinutes: number;
|
||||
cronExpression: string;
|
||||
}
|
||||
|
||||
export { generateRequestId, extractOrGenerateRequestId } from "./requestId";
|
||||
|
||||
37
packages/types/src/requestId.ts
Normal file
37
packages/types/src/requestId.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Generates a unique request ID for distributed tracing.
|
||||
* Uses crypto.randomUUID when available, falls back to a
|
||||
* timestamp-based UUID v4 format.
|
||||
*/
|
||||
export function generateRequestId(): string {
|
||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return (
|
||||
[0, 0, 0, 0, 0].map((_, i) => {
|
||||
const segment = i === 3 ? 8 : 4;
|
||||
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
|
||||
.toString(16)
|
||||
.padEnd(i === 4 ? 12 : 4, "0")
|
||||
.slice(0, i === 4 ? 12 : 4);
|
||||
}).join("-")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an existing request ID from headers or generates a new one.
|
||||
* Checks standard headers: X-Request-Id, X-Correlation-Id, X-Trace-Id.
|
||||
*/
|
||||
export function extractOrGenerateRequestId(headers: Record<string, string | string[] | undefined>): string {
|
||||
const candidates = ["x-request-id", "x-correlation-id", "x-trace-id"];
|
||||
for (const key of candidates) {
|
||||
const value = headers[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
if (Array.isArray(value) && value[0]) {
|
||||
return value[0].trim();
|
||||
}
|
||||
}
|
||||
return generateRequestId();
|
||||
}
|
||||
Reference in New Issue
Block a user