diff --git a/.env.example b/.env.example index 037f2c5..fc3abaa 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,6 @@ DATABASE_URL="YOUR_NOT_SO_SECRET_DATABASE_URL_GOES_HERE" LEMON_SQUEEZY_API_KEY="" # Just get it from somewhere LEMON_SQUEEZY_STORE_ID="" # Just get it from somewhere LEMON_SQUEEZY_VARIANT_ID="" # Just get it from somewhere +LEMON_SQUEEZY_WEBHOOK_SECRET= +TEST_API_KEY= +REDIS_URL= diff --git a/bun.lock b/bun.lock index ef08d9c..19315ec 100644 --- a/bun.lock +++ b/bun.lock @@ -7,14 +7,18 @@ "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@connectrpc/connect": "^2.1.0", - "@connectrpc/connect-node": "^2.1.0", + "@connectrpc/connect-fastify": "^2.1.1", + "@connectrpc/connect-node": "^2.1.1", "@connectrpc/validate": "^0.2.0", "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", "@libsql/client": "^0.15.15", "@types/expr-eval": "^1.1.2", + "bullmq": "^5.75.2", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "expr-eval": "^2.0.2", + "fastify": "^5.8.5", + "fastify-raw-body": "^5.0.0", "luxon": "^3.7.2", "mysql2": "^3.15.3", "pino": "^10.1.0", @@ -56,7 +60,9 @@ "@connectrpc/connect": ["@connectrpc/connect@2.1.0", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0" } }, "sha512-xhiwnYlJNHzmFsRw+iSPIwXR/xweTvTw8x5HiwWp10sbVtd4OpOXbRgE7V58xs1EC17fzusF1f5uOAy24OkBuA=="], - "@connectrpc/connect-node": ["@connectrpc/connect-node@2.1.0", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.1.0" } }, "sha512-6akCXZSX5uWHLR654ne9Tnq7AnPUkLS65NvgsI5885xBkcuVy2APBd8sA4sLqaplUt84cVEr6LhjEFNx6W1KtQ=="], + "@connectrpc/connect-fastify": ["@connectrpc/connect-fastify@2.1.1", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.1.1", "@connectrpc/connect-node": "2.1.1", "fastify": "^4.22.1 || ^5.1.0" } }, "sha512-UMkdus491H+iUTQE/wnZjaiX6oQ3+GH7gfE5kq2AtXZEZ4IV02bOmTYZS+jfu63+MMeyAkacgfruCYfvwqYzxA=="], + + "@connectrpc/connect-node": ["@connectrpc/connect-node@2.1.1", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.1.1" } }, "sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg=="], "@connectrpc/validate": ["@connectrpc/validate@0.2.0", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.9.0", "@bufbuild/protovalidate": "^1.0.0", "@connectrpc/connect": "^2.0.3" } }, "sha512-u1hdyt1WaFkKpuT/3J2wYlCjYLkYHMZamoatgxTIsV5Q8H9fO2px3zecmb0Q47YDjkZqnX6z0PAEstK+dNYiEQ=="], @@ -118,6 +124,20 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], + "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="], + + "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], + + "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], + + "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="], + + "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], + + "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="], + + "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@lemonsqueezy/lemonsqueezy.js": ["@lemonsqueezy/lemonsqueezy.js@4.0.0", "", {}, "sha512-xcY1/lDrY7CpIF98WKiL1ElsfoVhddP7FT0fw7ssOzrFqQsr44HgolKrQZxd9SywsCPn12OTOUieqDIokI3mFg=="], @@ -150,6 +170,18 @@ "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -238,22 +270,40 @@ "@vitest/utils": ["@vitest/utils@4.0.7", "", { "dependencies": { "@vitest/pretty-format": "4.0.7", "tinyrainbow": "^3.0.3" } }, "sha512-HNrg9CM/Z4ZWB6RuExhuC6FPmLipiShKVMnT9JlQvfhwR47JatWLChA6mtZqVHqypE6p/z6ofcjbyWpM7YLxPQ=="], + "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "buf": ["buf@github:bufbuild/buf#2e53c7d", {}, "bufbuild-buf-2e53c7d"], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bullmq": ["bullmq@5.75.2", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-5GDO2L5OfzogDxzJCeT7icYdI41vQZ5Wuw2z4EqpfPc1m4/gxyg0sEATULGPdR1sMw0kwCPqbehiIPseXxBX9A=="], + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "chai": ["chai@6.2.0", "", {}, "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], @@ -264,6 +314,10 @@ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], @@ -288,14 +342,34 @@ "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stringify": ["fast-json-stringify@6.3.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="], + + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastify": ["fastify@5.8.5", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q=="], + + "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], + + "fastify-raw-body": ["fastify-raw-body@5.0.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "raw-body": "^3.0.0", "secure-json-parse": "^2.4.0" } }, "sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="], + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -308,16 +382,34 @@ "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + "http-errors": ["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" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], + + "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="], + "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], + + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], @@ -334,16 +426,24 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -374,20 +474,42 @@ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="], + "safe-regex2": ["safe-regex2@5.1.1", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], @@ -408,6 +530,10 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], @@ -422,14 +548,24 @@ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="], "vitest": ["vitest@4.0.3", "", { "dependencies": { "@vitest/expect": "4.0.3", "@vitest/mocker": "4.0.3", "@vitest/pretty-format": "4.0.3", "@vitest/runner": "4.0.3", "@vitest/snapshot": "4.0.3", "@vitest/spy": "4.0.3", "@vitest/utils": "4.0.3", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.19", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.3", "@vitest/browser-preview": "4.0.3", "@vitest/browser-webdriverio": "4.0.3", "@vitest/ui": "4.0.3", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA=="], @@ -460,6 +596,10 @@ "@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.0.7", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q=="], + "fastify-raw-body/secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + + "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "vitest/@vitest/utils": ["@vitest/utils@4.0.3", "", { "dependencies": { "@vitest/pretty-format": "4.0.3", "tinyrainbow": "^3.0.3" } }, "sha512-qV6KJkq8W3piW6MDIbGOmn1xhvcW4DuA07alqaQ+vdx7YA49J85pnwnxigZVQFQw3tWnQNRKWwhz5wbP6iv/GQ=="], "@bufbuild/protoc-gen-es/@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], diff --git a/package.json b/package.json index 1e48882..9420055 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "@bufbuild/protoc-gen-connect-es": "^0.13.0", "@bufbuild/protoc-gen-es": "^2.9.0", "@types/bun": "latest", + "@types/expr-eval": "^1.1.2", "@types/luxon": "^3.7.1", "@vitest/ui": "^4.0.3", "buf": "bufbuild/buf", "drizzle-kit": "^0.31.6", "tsx": "^4.20.6", + "skills": "^1.3.1", "vitest": "^4.0.3" }, "peerDependencies": { @@ -31,20 +33,20 @@ "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@connectrpc/connect": "^2.1.0", - "@connectrpc/connect-node": "^2.1.0", + "@connectrpc/connect-fastify": "^2.1.1", + "@connectrpc/connect-node": "^2.1.1", "@connectrpc/validate": "^0.2.0", "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", - "@libsql/client": "^0.15.15", - "@types/expr-eval": "^1.1.2", + "bullmq": "^5.75.2", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "expr-eval": "^2.0.2", + "fastify": "^5.8.5", + "fastify-raw-body": "^5.0.0", "luxon": "^3.7.2", - "mysql2": "^3.15.3", "pino": "^10.1.0", "pino-pretty": "^13.1.2", "postgres": "^3.4.7", - "skills": "^1.3.1", "zod": "^4.1.12" } } diff --git a/src/errors/internals.ts b/src/errors/internals.ts new file mode 100644 index 0000000..22037bd --- /dev/null +++ b/src/errors/internals.ts @@ -0,0 +1,73 @@ +import { Code, ConnectError } from "@connectrpc/connect"; + +export enum InternalsErrorType { + INVALID_CRON = "INVALID_CRON", + QUEUE_CREATION_FAILED = "QUEUE_CREATION_FAILED", + VALIDATION_FAILED = "VALIDATION_FAILED", + UNKNOWN = "UNKNOWN", +} + +export interface InternalsErrorContext { + type: InternalsErrorType; + message: string; + originalError?: Error; + code: Code; +} + +export class InternalsError extends ConnectError { + readonly type: InternalsErrorType; + readonly originalError?: Error; + + constructor(context: InternalsErrorContext) { + super(context.message, context.code); + this.name = "InternalsError"; + this.type = context.type; + this.originalError = context.originalError; + + Object.setPrototypeOf(this, InternalsError.prototype); + } + + static invalidCron(details?: string, originalError?: Error): InternalsError { + return new InternalsError({ + type: InternalsErrorType.INVALID_CRON, + message: details + ? `Invalid cron expression: ${details}` + : "Invalid cron expression", + code: Code.InvalidArgument, + originalError, + }); + } + + static queueCreationFailed( + details?: string, + originalError?: Error + ): InternalsError { + return new InternalsError({ + type: InternalsErrorType.QUEUE_CREATION_FAILED, + message: details + ? `Failed to create queue: ${details}` + : "Failed to create queue", + code: Code.Internal, + originalError, + }); + } + + static validationFailed(details: string, originalError?: Error): InternalsError { + return new InternalsError({ + type: InternalsErrorType.VALIDATION_FAILED, + message: `Internals validation failed: ${details}`, + code: Code.InvalidArgument, + originalError, + }); + } + + static unknown(originalError?: Error): InternalsError { + const details = originalError?.message || "No details available"; + return new InternalsError({ + type: InternalsErrorType.UNKNOWN, + message: `Unexpected internals error: ${details}`, + code: Code.Internal, + originalError, + }); + } +} \ No newline at end of file diff --git a/src/events/RawEvents/Metadata.ts b/src/events/RawEvents/Metadata.ts new file mode 100644 index 0000000..31cf5a3 --- /dev/null +++ b/src/events/RawEvents/Metadata.ts @@ -0,0 +1,21 @@ +import type { MetadataEvent, MetadataEventData } from "../../interface/event/Event"; +import { DateTime } from "luxon"; + +export class Metadata implements MetadataEvent { + public reported_timestamp: DateTime; + public readonly type = "METADATA" as const; + + constructor(public data: MetadataEventData) { + this.reported_timestamp = DateTime.utc(); + } + + serialize() { + return { + SQL: { + type: this.type, + reported_timestamp: this.reported_timestamp, + data: this.data, + }, + }; + } +} diff --git a/src/factory/EventStorageAdapterFactory.ts b/src/factory/EventStorageAdapterFactory.ts index 66471c8..690fdff 100644 --- a/src/factory/EventStorageAdapterFactory.ts +++ b/src/factory/EventStorageAdapterFactory.ts @@ -1,12 +1,6 @@ import type { EventKind } from "../interface/event/Event.ts"; import { PostgresAdapter } from "../storage/adapter/postgres/postgres.ts"; -/** - * StorageAdapterFactory - Facade for the new SQL adapter factory - * - * Maintains backward compatibility while delegating to the new - * dependency-injected SQL adapter factory - */ export class StorageAdapterFactory { /** * Get the appropriate storage adapter for a given event @@ -29,6 +23,9 @@ export class StorageAdapterFactory { case "ADD_KEY": { return new PostgresAdapter(); } + case "METADATA": { + return new PostgresAdapter(); + } default: { throw new Error(`Unknown event type: ${RequestType}`); } diff --git a/src/interceptors/connectInterceptors.ts b/src/interceptors/connectInterceptors.ts new file mode 100644 index 0000000..3cda2a5 --- /dev/null +++ b/src/interceptors/connectInterceptors.ts @@ -0,0 +1,8 @@ +import type { Interceptor } from "@connectrpc/connect"; +import { createValidateInterceptor } from "@connectrpc/validate"; +import { loggingInterceptor } from "./logging.ts"; +import { authInterceptor } from "./auth.ts"; + +export function createConnectInterceptors(): Interceptor[] { + return [loggingInterceptor(), createValidateInterceptor(), authInterceptor()]; +} diff --git a/src/interface/event/Event.ts b/src/interface/event/Event.ts index 1f3f435..2b37545 100644 --- a/src/interface/event/Event.ts +++ b/src/interface/event/Event.ts @@ -27,6 +27,11 @@ export type PaymentEventData = { creditAmount: number; }; +export type MetadataEventData = { + payment_cron: string; + payment_webhook: string | null; +}; + /** * Event kind discriminator */ @@ -35,6 +40,7 @@ export type EventKind = | "AI_TOKEN_USAGE" | "ADD_KEY" | "PAYMENT" + | "METADATA"; /** * Mapping of event kinds to their data structures @@ -44,6 +50,7 @@ export type EventDataMap = { AI_TOKEN_USAGE: AITokenUsageEventData; ADD_KEY: AddKeyEventData; PAYMENT: PaymentEventData; + METADATA: MetadataEventData; }; /** @@ -75,6 +82,7 @@ type SqlRecordMap = { SDK_CALL: SqlRecordWithUserId<"SDK_CALL">; AI_TOKEN_USAGE: SqlRecordWithUserId<"AI_TOKEN_USAGE">; PAYMENT: SqlRecordWithUserId<"PAYMENT">; + METADATA: BaseSqlRecord<"METADATA">; }; /** @@ -125,3 +133,8 @@ export interface AddKeyEvent extends Event<"ADD_KEY"> {} export interface PaymentEvent extends Event<"PAYMENT"> { readonly userId: UserId; } + +/** + * Metadata Event + */ +export interface MetadataEvent extends Event<"METADATA"> {} diff --git a/src/interface/storage/Storage.ts b/src/interface/storage/Storage.ts index 68100a4..6cf1e24 100644 --- a/src/interface/storage/Storage.ts +++ b/src/interface/storage/Storage.ts @@ -9,7 +9,7 @@ export interface StorageAdapter { add( serialized: SerializedEvent, - apiKeyId: string + apiKeyId?: string ): Promise<{ id: string } | void>; price(userID: UserId, event_type: EventKind): Promise; } diff --git a/src/queues/onboarding.ts b/src/queues/onboarding.ts new file mode 100644 index 0000000..54b50d9 --- /dev/null +++ b/src/queues/onboarding.ts @@ -0,0 +1,39 @@ +import { Queue, type RepeatOptions } from "bullmq"; +import { DateTime } from "luxon"; +import { getRedisConnection } from "../storage/db/redis.ts"; + +export interface OnboardingJobData { + cronExpression: string; + createdAt: string; +} + +let onboardingQueue: Queue | null = null; + +export function getOnboardingQueue(): Queue { + if (!onboardingQueue) { + onboardingQueue = new Queue("onboarding", { + connection: getRedisConnection(), + }); + } + return onboardingQueue; +} + +export async function addOnboardingCronJob( + cronExpression: string +): Promise { + const repeatOptions: RepeatOptions = { + pattern: cronExpression, + }; + + const queue = getOnboardingQueue(); + await queue.add( + `onboarding-${cronExpression}`, + { + cronExpression, + createdAt: DateTime.utc().toISO(), + }, + { + repeat: repeatOptions, + } + ); +} \ No newline at end of file diff --git a/src/routes/gRPC/registerRoutes.ts b/src/routes/gRPC/registerRoutes.ts new file mode 100644 index 0000000..f30503b --- /dev/null +++ b/src/routes/gRPC/registerRoutes.ts @@ -0,0 +1,23 @@ +import type { ConnectRouter } from "@connectrpc/connect"; +import { EventService } from "../../gen/event/v1/event_pb.ts"; +import { AuthService } from "../../gen/auth/v1/auth_pb.ts"; +import { PaymentService } from "../../gen/payment/v1/payment_pb.ts"; +import { registerEvent } from "./events/registerEvent.ts"; +import { streamEvents } from "./events/streamEvents.ts"; +import { createAPIKey } from "./auth/createAPIKey.ts"; +import { createCheckoutLink } from "./payment/createCheckoutLink.ts"; + +export function registerGrpcRoutes(router: ConnectRouter): void { + router.service(AuthService, { + createAPIKey, + }); + + router.service(EventService, { + registerEvent, + streamEvents, + }); + + router.service(PaymentService, { + createCheckoutLink, + }); +} diff --git a/src/routes/http/api/onboarding.ts b/src/routes/http/api/onboarding.ts new file mode 100644 index 0000000..45bc25f --- /dev/null +++ b/src/routes/http/api/onboarding.ts @@ -0,0 +1,80 @@ +import type { FastifyRequest, FastifyReply } from "fastify"; +import { ZodError } from "zod"; +import { + onboardingCronSchema, + type OnboardingCronSchemaType, +} from "../../../zod/internals.ts"; +import { addOnboardingCronJob } from "../../../queues/onboarding.ts"; +import { + createWideEventBuilder, + generateRequestId, +} from "../../../context/requestContext.ts"; +import { logger } from "../../../errors/logger.ts"; +import { StorageAdapterFactory } from "../../../factory/index.ts"; +import { Metadata } from "../../../events/RawEvents/Metadata.ts"; + +export async function handleOnboarding( + request: FastifyRequest, + reply: FastifyReply +): Promise<{ crons: string[] }> { + const builder = createWideEventBuilder( + generateRequestId(), + request.method, + request.url + ); + + try { + const body = await request.body; + const validated = onboardingCronSchema.parse(body); + + const crons: string[] = []; + + for (const cronExpression of validated.crons) { + await addOnboardingCronJob(cronExpression); + crons.push(cronExpression); + } + + const webhookUrl = validated.webhookUrl && validated.webhookUrl !== "" + ? validated.webhookUrl + : null; + + const metadataEvent = new Metadata({ + payment_cron: crons.join(","), + payment_webhook: webhookUrl, + }); + + const adapter = await StorageAdapterFactory.getEventStorageAdapter( + metadataEvent.type + ); + await adapter.add(metadataEvent.serialize()); + + builder.setSuccess(200).addContext({ + cronCount: crons.length, + }); + + reply.code(201); + return { crons }; + } catch (error) { + if (error instanceof ZodError) { + const issues = error.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join("; "); + builder.setError(400, { + type: "ValidationError", + message: issues, + }); + reply.code(400); + return { crons: [] }; + } + + const err = error instanceof Error ? error : new Error(String(error)); + builder.setError(500, { + type: "InternalError", + message: err.message, + }); + reply.code(500); + return { crons: [] }; + } finally { + logger.emit(builder.build()); + } +} diff --git a/src/routes/http/api/registerApiRoutes.ts b/src/routes/http/api/registerApiRoutes.ts new file mode 100644 index 0000000..4775d02 --- /dev/null +++ b/src/routes/http/api/registerApiRoutes.ts @@ -0,0 +1,16 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { handleOnboarding } from "./onboarding.ts"; + +export async function registerApiRoutes( + server: ReturnType +): Promise { + server.post( + "/api/v1/internals/onboarding", + async ( + request: FastifyRequest, + reply: FastifyReply + ) => { + return handleOnboarding(request, reply); + } + ); +} \ No newline at end of file diff --git a/src/routes/http/createdCheckout.ts b/src/routes/http/createdCheckout.ts index a2b9f6d..ef6dd1b 100644 --- a/src/routes/http/createdCheckout.ts +++ b/src/routes/http/createdCheckout.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import type { IncomingMessage, ServerResponse } from "node:http"; import { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js"; import { Payment } from "../../events/RawEvents/Payment.ts"; import { StorageAdapterFactory } from "../../factory/EventStorageAdapterFactory.ts"; @@ -40,6 +39,11 @@ interface LemonSqueezyWebhookPayload { }; } +interface WebhookResponse { + statusCode: number; + body: { message?: string; error?: string }; +} + /** * Verifies the webhook signature from Lemon Squeezy */ @@ -63,41 +67,17 @@ function verifyWebhookSignature( } } -/** - * Reads the request body as a string - */ -function readBody(req: IncomingMessage): Promise { - return new Promise((resolve, reject) => { - let body = ""; - req.on("data", (chunk: Buffer) => { - body += chunk.toString(); - }); - req.on("end", () => { - resolve(body); - }); - req.on("error", (error: Error) => { - reject(error); - }); - }); -} - /** * Handles the Lemon Squeezy order-created webhook. * This handler is designed to work with the HTTP logging middleware, * which provides a WideEventBuilder for adding business context. */ export async function handleLemonSqueezyWebhook( - req: IncomingMessage, - res: ServerResponse, + rawBody: string, + signature: string | undefined, builder: WideEventBuilder -): Promise { +): Promise { try { - // Read the raw body - const rawBody = await readBody(req); - - // Verify webhook signature - const signature = req.headers["x-signature"] as string | undefined; - // Read webhook secret at runtime for testability const LEMON_SQUEEZY_WEBHOOK_SECRET = process.env.LEMON_SQUEEZY_WEBHOOK_SECRET; @@ -107,9 +87,10 @@ export async function handleLemonSqueezyWebhook( type: "ConfigurationError", message: "Webhook secret not configured", }); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Webhook secret not configured" })); - return; + return { + statusCode: 500, + body: { error: "Webhook secret not configured" }, + }; } const isValid = verifyWebhookSignature( @@ -123,52 +104,57 @@ export async function handleLemonSqueezyWebhook( type: "AuthenticationError", message: "Invalid signature", }); - res.writeHead(401, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Invalid signature" })); - return; + return { statusCode: 401, body: { error: "Invalid signature" } }; } - // Parse the payload - let payload: LemonSqueezyWebhookPayload; + let webhookPayload: LemonSqueezyWebhookPayload; try { - payload = JSON.parse(rawBody); + webhookPayload = JSON.parse(rawBody) as LemonSqueezyWebhookPayload; } catch { builder.setError(400, { type: "ParseError", message: "Invalid JSON payload", }); - res.writeHead(400, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Invalid JSON payload" })); - return; + return { statusCode: 400, body: { error: "Invalid JSON payload" } }; + } + + if (!webhookPayload.meta || !webhookPayload.data?.attributes) { + builder.setError(400, { + type: "ParseError", + message: "Invalid webhook payload shape", + }); + return { + statusCode: 400, + body: { error: "Invalid webhook payload shape" }, + }; } // Add webhook event context builder.setWebhookContext({ - webhookEvent: payload.meta.event_name, - orderId: payload.data.id, + webhookEvent: webhookPayload.meta.event_name, + orderId: webhookPayload.data.id, }); // Handle only order-created events - if (payload.meta.event_name !== "order_created") { + if (webhookPayload.meta.event_name !== "order_created") { builder.setSuccess(200); builder.addContext({ ignored: true }); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ message: "Event ignored" })); - return; + return { statusCode: 200, body: { message: "Event ignored" } }; } // Extract user ID from custom data - const userId = payload.meta.custom_data?.user_id; - const apiKeyId = payload.meta.custom_data?.api_key_id; + const userId = webhookPayload.meta.custom_data?.user_id; + const apiKeyId = webhookPayload.meta.custom_data?.api_key_id; if (!userId) { builder.setError(400, { type: "ValidationError", message: "Missing user_id in webhook payload", }); - res.writeHead(400, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Missing user_id in webhook payload" })); - return; + return { + statusCode: 400, + body: { error: "Missing user_id in webhook payload" }, + }; } if (!apiKeyId) { @@ -176,16 +162,17 @@ export async function handleLemonSqueezyWebhook( type: "ValidationError", message: "Missing apiKeyId in webhook payload", }); - res.writeHead(400, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Missing apiKeyId in webhook payload" })); - return; + return { + statusCode: 400, + body: { error: "Missing apiKeyId in webhook payload" }, + }; } // Add user and payment context to wide event builder.setUser(userId); // Extract payment amount (convert from cents to the integer format used in DB) - const creditAmount = Math.round(payload.data.attributes.total); + const creditAmount = Math.round(webhookPayload.data.attributes.total); builder.setPaymentContext({ creditAmount }); // Create and store the payment event @@ -197,10 +184,10 @@ export async function handleLemonSqueezyWebhook( await adapter.add(paymentEvent.serialize(), apiKeyId); builder.setSuccess(200); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ message: "Webhook processed successfuladdEvent " }) - ); + return { + statusCode: 200, + body: { message: "Webhook processed successfully" }, + }; } catch (dbError) { const errorMessage = dbError instanceof Error ? dbError.message : String(dbError); @@ -210,8 +197,7 @@ export async function handleLemonSqueezyWebhook( cause: dbError instanceof Error ? dbError.message : undefined, stack: isDev && dbError instanceof Error ? dbError.stack : undefined, }); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Database error" })); + return { statusCode: 500, body: { error: "Database error" } }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -221,7 +207,6 @@ export async function handleLemonSqueezyWebhook( cause: error instanceof Error ? error.message : undefined, stack: isDev && error instanceof Error ? error.stack : undefined, }); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Internal server error" })); + return { statusCode: 500, body: { error: "Internal server error" } }; } } diff --git a/src/routes/http/registerWebhookRoutes.ts b/src/routes/http/registerWebhookRoutes.ts new file mode 100644 index 0000000..8844e4c --- /dev/null +++ b/src/routes/http/registerWebhookRoutes.ts @@ -0,0 +1,62 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { createWideEventBuilder, generateRequestId } from "../../context/requestContext.ts"; +import { logger } from "../../errors/logger.ts"; +import { handleLemonSqueezyWebhook } from "./createdCheckout.ts"; + +export async function registerWebhookRoutes( + server: ReturnType +): Promise { + server.post( + "/webhooks/lemonsqueezy/createdCheckout", + { config: { rawBody: true } }, + async ( + request: FastifyRequest, + reply: FastifyReply + ) => { + const builder = createWideEventBuilder( + generateRequestId(), + request.method, + request.url + ); + + try { + const signatureHeader = request.headers["x-signature"]; + const signature = + typeof signatureHeader === "string" + ? signatureHeader + : Array.isArray(signatureHeader) + ? signatureHeader[0] + : undefined; + + const requestWithRawBody = request as typeof request & { + rawBody?: string; + }; + const rawBody = requestWithRawBody.rawBody; + + if (!rawBody) { + builder.setError(400, { + type: "ParseError", + message: "Missing raw webhook payload", + }); + reply.code(400); + return { error: "Missing raw webhook payload" }; + } + + const result = await handleLemonSqueezyWebhook(rawBody, signature, builder); + + reply.code(result.statusCode); + return result.body; + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + builder.setError(500, { + type: "InternalError", + message: err.message, + }); + reply.code(500); + return { error: "Internal server error" }; + } finally { + logger.emit(builder.build()); + } + } + ); +} diff --git a/src/server.ts b/src/server.ts index b4e6608..cb4c728 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,24 +1,13 @@ -import * as http from "node:http"; -import * as http2 from "node:http2"; -import type { ConnectRouter } from "@connectrpc/connect"; -import { connectNodeAdapter } from "@connectrpc/connect-node"; -import { createValidateInterceptor } from "@connectrpc/validate"; -import { EventService } from "./gen/event/v1/event_pb.ts"; -import { AuthService } from "./gen/auth/v1/auth_pb.ts"; -import { PaymentService } from "./gen/payment/v1/payment_pb.ts"; -import { loggingInterceptor } from "./interceptors/logging.ts"; -import { authInterceptor } from "./interceptors/auth.ts"; -import { registerEvent } from "./routes/gRPC/events/registerEvent.ts"; -import { streamEvents } from "./routes/gRPC/events/streamEvents.ts"; -import { createAPIKey } from "./routes/gRPC/auth/createAPIKey.ts"; -import { createCheckoutLink } from "./routes/gRPC/payment/createCheckoutLink.ts"; import { getPostgresDB } from "./storage/db/postgres/db.ts"; -import { handleLemonSqueezyWebhook } from "./routes/http/createdCheckout.ts"; -import { withHttpLogging } from "./middleware/httpLogging.ts"; import { logger } from "./errors/logger.ts"; +import { startRawGrpcServer } from "./servers/rawGrpcServer.ts"; +import { startFastifyServer } from "./servers/fastifyServer.ts"; +import { OnboardingWorker } from "./workers/onboarding.ts"; +import { getRedisConnection } from "./storage/db/redis.ts"; const DATABASE_URL = process.env.DATABASE_URL; const HMAC_SECRET = process.env.HMAC_SECRET; +const REDIS_URL = process.env.REDIS_URL; if (!DATABASE_URL) { logger.fatal("DATABASE_URL is not defined in environment variables"); @@ -30,68 +19,31 @@ if (!HMAC_SECRET) { throw new Error("HMAC_SECRET environment variable is not set"); } +if (!REDIS_URL) { + logger.fatal("REDIS_URL environmentvariable is not set"); + throw new Error("REDIS_URL environmentvariable is not set"); +} + getPostgresDB(DATABASE_URL); +getRedisConnection(REDIS_URL); -const grpcHandler = connectNodeAdapter({ - interceptors: [ - loggingInterceptor(), // First - captures all requests including auth failures - createValidateInterceptor(), - authInterceptor(), - ], - routes: (router: ConnectRouter) => { - // EventService implementation - router.service(EventService, { - registerEvent, - streamEvents, - }); +const PORT = Number(process.env.PORT ?? 8069); +const GRPC_PORT = Number(process.env.GRPC_PORT ?? 8070); - // AuthService implementation - router.service(AuthService, { - createAPIKey, - }); +let onboardingWorker: OnboardingWorker | undefined; - // PaymentService implementation - router.service(PaymentService, { - createCheckoutLink, - }); - }, -}); +async function main(): Promise { + startRawGrpcServer(GRPC_PORT); + await startFastifyServer(PORT, GRPC_PORT); -// Wrap webhook handler with HTTP logging middleware -const webhookHandler = withHttpLogging(handleLemonSqueezyWebhook); + onboardingWorker = new OnboardingWorker(); + logger.lifecycle("Onboarding worker started"); +} -// Create a combined handler for both gRPC and HTTP webhooks -const requestHandler = ( - req: http.IncomingMessage | http2.Http2ServerRequest, - res: http.ServerResponse | http2.Http2ServerResponse -) => { - // Handle webhook endpoint - if ( - req.url === "/webhooks/lemonsqueezy/createdCheckout" && - req.method === "POST" - ) { - webhookHandler( - req as unknown as http.IncomingMessage, - res as unknown as http.ServerResponse - ); - return; +process.on("beforeExit", async () => { + if (onboardingWorker) { + await onboardingWorker.close(); } - - // Handle all other requests as gRPC - grpcHandler(req, res); -}; - -const PORT = Number(process.env.PORT ?? 8069); - -http2.createServer(requestHandler).listen(PORT); - -logger.lifecycle("Server started", { - grpcH2Port: PORT, - env: process.env.NODE_ENV || "development", -}); -logger.lifecycle("Webhook endpoint available", { - url: `http://localhost:${PORT}/webhooks/lemonsqueezy/createdCheckout`, -}); -logger.lifecycle("gRPC h2c endpoint available", { - url: `http://localhost:${PORT}`, }); + +void main(); diff --git a/src/servers/fastifyServer.ts b/src/servers/fastifyServer.ts new file mode 100644 index 0000000..e159158 --- /dev/null +++ b/src/servers/fastifyServer.ts @@ -0,0 +1,51 @@ +import { fastify } from "fastify"; +import fastifyRawBody from "fastify-raw-body"; +import { fastifyConnectPlugin } from "@connectrpc/connect-fastify"; +import { registerGrpcRoutes } from "../routes/gRPC/registerRoutes.ts"; +import { createConnectInterceptors } from "../interceptors/connectInterceptors.ts"; +import { registerWebhookRoutes } from "../routes/http/registerWebhookRoutes.ts"; +import { registerApiRoutes } from "../routes/http/api/registerApiRoutes.ts"; +import { logger } from "../errors/logger.ts"; + +export async function startFastifyServer(port: number, grpcPort: number): Promise { + const server = fastify({ + http2: true, + }); + + await server.register(fastifyConnectPlugin, { + interceptors: createConnectInterceptors(), + routes: registerGrpcRoutes, + }); + + await server.register(fastifyRawBody, { + field: "rawBody", + global: false, + encoding: "utf8", + runFirst: true, + }); + + server.get("/", async (_request, reply) => { + reply.type("text/plain"); + return "Hello World!"; + }); + + await registerWebhookRoutes(server); + await registerApiRoutes(server); + + await server.listen({ host: "localhost", port }); + + logger.lifecycle("Server started", { + httpPort: port, + grpcH2Port: grpcPort, + env: process.env.NODE_ENV || "development", + }); + logger.lifecycle("Webhook endpoint available", { + url: `http://localhost:${port}/webhooks/lemonsqueezy/createdCheckout`, + }); + logger.lifecycle("API endpoint available", { + url: `http://localhost:${port}/api/v1/internals/onboarding`, + }); + logger.lifecycle("Connect endpoint available", { + url: `http://localhost:${port}`, + }); +} diff --git a/src/servers/rawGrpcServer.ts b/src/servers/rawGrpcServer.ts new file mode 100644 index 0000000..1f5c718 --- /dev/null +++ b/src/servers/rawGrpcServer.ts @@ -0,0 +1,18 @@ +import * as http2 from "node:http2"; +import { connectNodeAdapter } from "@connectrpc/connect-node"; +import { registerGrpcRoutes } from "../routes/gRPC/registerRoutes.ts"; +import { createConnectInterceptors } from "../interceptors/connectInterceptors.ts"; +import { logger } from "../errors/logger.ts"; + +export function startRawGrpcServer(grpcPort: number): void { + const grpcHandler = connectNodeAdapter({ + interceptors: createConnectInterceptors(), + routes: registerGrpcRoutes, + }); + + http2.createServer(grpcHandler).listen(grpcPort); + + logger.lifecycle("Raw gRPC h2c endpoint available", { + url: `http://localhost:${grpcPort}`, + }); +} diff --git a/src/storage/adapter/postgres/handlers/addMetadata.ts b/src/storage/adapter/postgres/handlers/addMetadata.ts new file mode 100644 index 0000000..0b8dfc1 --- /dev/null +++ b/src/storage/adapter/postgres/handlers/addMetadata.ts @@ -0,0 +1,52 @@ +import { getPostgresDB } from "../../../db/postgres/db"; +import { metadataTable } from "../../../db/postgres/schema"; +import { StorageError } from "../../../../errors/storage"; +import { type SqlRecord } from "../../../../interface/event/Event"; +import { eq } from "drizzle-orm"; + +export async function handleAddMetadata( + event_data: SqlRecord<"METADATA"> +): Promise { + const connectionObject = getPostgresDB(); + + const paymentCron = event_data?.data?.payment_cron; + const paymentWebhook = event_data?.data?.payment_webhook; + + if (!paymentCron || paymentCron.trim().length === 0) { + throw StorageError.invalidData("Invalid payment_cron: value is required"); + } + + if (paymentWebhook !== null && typeof paymentWebhook !== "string") { + throw StorageError.invalidData( + "Invalid payment_webhook: must be a string or null" + ); + } + + try { + const [existingMetadata] = await connectionObject + .select({ id: metadataTable.id }) + .from(metadataTable) + .limit(1); + + if (existingMetadata) { + await connectionObject + .update(metadataTable) + .set({ + payment_cron: paymentCron, + payment_webhook: paymentWebhook, + }) + .where(eq(metadataTable.id, existingMetadata.id)); + return; + } + + await connectionObject.insert(metadataTable).values({ + payment_cron: paymentCron, + payment_webhook: paymentWebhook, + }); + } catch (e) { + throw StorageError.insertFailed( + "Failed to upsert metadata record", + e instanceof Error ? e : new Error(String(e)) + ); + } +} diff --git a/src/storage/adapter/postgres/handlers/index.ts b/src/storage/adapter/postgres/handlers/index.ts index 72639cb..e4e13b1 100644 --- a/src/storage/adapter/postgres/handlers/index.ts +++ b/src/storage/adapter/postgres/handlers/index.ts @@ -5,3 +5,4 @@ export { handlePriceRequestPayment } from "./priceRequestPayment"; export { handlePriceRequestSdkCall } from "./priceRequestSdkCall"; export { handleAddAiTokenUsage } from "./addAiTokenUsage"; export { handlePriceRequestAiTokenUsage } from "./priceRequestAiTokenUsage"; +export { handleAddMetadata } from "./addMetadata"; diff --git a/src/storage/adapter/postgres/postgres.ts b/src/storage/adapter/postgres/postgres.ts index 94c8c15..6112a74 100644 --- a/src/storage/adapter/postgres/postgres.ts +++ b/src/storage/adapter/postgres/postgres.ts @@ -10,6 +10,7 @@ import { handlePriceRequestSdkCall, handleAddAiTokenUsage, handlePriceRequestAiTokenUsage, + handleAddMetadata, } from "./handlers"; import type { SerializedEvent, @@ -21,7 +22,7 @@ import type { UserId } from "../../../config/identifiers"; export class PostgresAdapter implements StorageAdapter { connectionObject = getPostgresDB(); - async add(serialized: SerializedEvent, apiKeyId: string) { + async add(serialized: SerializedEvent, apiKeyId?: string) { let event_data: SqlRecord; try { @@ -72,6 +73,10 @@ export class PostgresAdapter implements StorageAdapter { return await handleAddPayment(event_data, apiKeyId); } + case "METADATA": { + return await handleAddMetadata(event_data); + } + default: { //@ts-ignore throw StorageError.unknownEventType(event_data.type); diff --git a/src/storage/db/postgres/schema.ts b/src/storage/db/postgres/schema.ts index 6d3aaba..47074b8 100644 --- a/src/storage/db/postgres/schema.ts +++ b/src/storage/db/postgres/schema.ts @@ -123,7 +123,7 @@ export const paymentEventsRelation = relations( export const tagsTable = pgTable("tags", { id: uuid("id").primaryKey().defaultRandom(), - tag: text("key").notNull(), + key: text("key").notNull(), amount: integer("amount").notNull(), }); @@ -147,3 +147,9 @@ export const aiTokenUsageEventsRelation = relations( }), }) ); + +export const metadataTable = pgTable("metadata", { + id: uuid("id").primaryKey().defaultRandom(), + payment_cron: text("payment_cron").notNull(), + payment_webhook: text("payment_webhook"), +}); diff --git a/src/storage/db/redis.ts b/src/storage/db/redis.ts new file mode 100644 index 0000000..a0c1cda --- /dev/null +++ b/src/storage/db/redis.ts @@ -0,0 +1,18 @@ +import IORedis from "ioredis"; + +let redisConnection: IORedis | null = null; + +export function getRedisConnection(REDIS_URL?: string): IORedis { + if (redisConnection) return redisConnection; + + if (!REDIS_URL) { + throw new Error("REDIS_URL is not defined"); + } + + redisConnection = new IORedis(REDIS_URL, { + maxRetriesPerRequest: null, + lazyConnect: true, + }); + + return redisConnection; +} \ No newline at end of file diff --git a/src/utils/fetchTagAmount.ts b/src/utils/fetchTagAmount.ts index 6da969f..e92eef6 100644 --- a/src/utils/fetchTagAmount.ts +++ b/src/utils/fetchTagAmount.ts @@ -17,7 +17,7 @@ export async function fetchTagAmount( const [tagRow] = await db .select() .from(tagsTable) - .where(eq(tagsTable.tag, tag)) + .where(eq(tagsTable.key, tag)) .limit(1); if (!tagRow) { diff --git a/src/workers/onboarding.ts b/src/workers/onboarding.ts new file mode 100644 index 0000000..430f3da --- /dev/null +++ b/src/workers/onboarding.ts @@ -0,0 +1,66 @@ +import { Worker, Job } from "bullmq"; +import { DateTime } from "luxon"; +import { getRedisConnection } from "../storage/db/redis.ts"; +import { getPostgresDB } from "../storage/db/postgres/db.ts"; +import { metadataTable } from "../storage/db/postgres/schema.ts"; +import { logger } from "../errors/logger.ts"; + +export class OnboardingWorker { + private worker: Worker; + + constructor() { + this.worker = new Worker( + "onboarding", + async (job: Job) => { + await this.processJob(job); + }, + { + connection: getRedisConnection(), + concurrency: 1, + } + ); + } + + private async processJob(job: Job): Promise { + const db = getPostgresDB(); + const [metadata] = await db.select().from(metadataTable).limit(1); + + if (!metadata) { + logger.lifecycle("[onboarding] No metadata found, skipping"); + return; + } + + if (!metadata.payment_webhook) { + logger.lifecycle("[onboarding] No webhook configured, skipping"); + return; + } + + const timestamp = DateTime.utc().toISO(); + + try { + const response = await fetch(metadata.payment_webhook, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ timestamp }), + }); + + if (!response.ok) { + logger.fatal( + `[onboarding] Webhook failed: ${response.status} ${response.statusText}` + ); + return; + } + + logger.lifecycle(`[onboarding] Webhook triggered at ${timestamp}`); + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)); + logger.fatal(`[onboarding] Webhook error: ${err.message}`, err); + } + } + + async close(): Promise { + await this.worker.close(); + } +} diff --git a/src/zod/internals.ts b/src/zod/internals.ts new file mode 100644 index 0000000..2c4aa6b --- /dev/null +++ b/src/zod/internals.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +const cronField = z + .string() + .min(9, "Cron expression must be at least 9 characters") + .max(100, "Cron expression must be less than 100 characters") + .refine((val) => { + const parts = val.trim().split(/\s+/); + if (parts.length < 5 || parts.length > 6) return false; + return true; + }, "Cron expression must have 5 or 6 fields (minute hour day month weekday or with seconds)"); + +export const onboardingCronSchema = z.object({ + crons: z + .array(cronField) + .min(1, "At least one cron expression is required") + .max(100, "Maximum 100 cron expressions allowed"), + webhookUrl: z.url("Invalid webhook URL").or(z.literal("")), +}); + +export type OnboardingCronSchemaType = z.infer; \ No newline at end of file