diff --git a/bun.lock b/bun.lock index 801bcfae..4e0da45f 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "dependencies": { "@aws-sdk/client-iot-data-plane": "^3.758.0", "@aws-sdk/client-sesv2": "^3.753.0", + "@neondatabase/serverless": "^1.0.1", "@openauthjs/openauth": "*", "@openauthjs/openevent": "^0.0.27", "@polar-sh/sdk": "^0.26.1", @@ -36,6 +37,7 @@ "sanitize-html": "^2.16.0", "sharp": "^0.34.1", "steam-session": "*", + "ws": "^8.18.3", "xml2js": "^0.6.2", }, "devDependencies": { @@ -645,6 +647,8 @@ "@multiformats/multiaddr-to-uri": ["@multiformats/multiaddr-to-uri@11.0.2", "", { "dependencies": { "@multiformats/multiaddr": "^12.3.0" } }, "sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg=="], + "@neondatabase/serverless": ["@neondatabase/serverless@1.0.1", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-O6yC5TT0jbw86VZVkmnzCZJB0hfxBl0JJz6f+3KHoZabjb/X08r9eFA+vuY06z1/qaovykvdkrXYq3SPUuvogA=="], + "@nestri/core": ["@nestri/core@workspace:cloud/packages/core"], "@nestri/functions": ["@nestri/functions@workspace:cloud/packages/functions"], @@ -1171,7 +1175,7 @@ "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + "@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="], "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], @@ -3053,7 +3057,7 @@ "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], - "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], @@ -3469,6 +3473,8 @@ "@fastify/ajv-compiler/ajv": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@fastify/websocket/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "@gerrit0/mini-shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.0", "", { "dependencies": { "@shikijs/types": "3.4.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ=="], "@gerrit0/mini-shiki/@shikijs/langs": ["@shikijs/langs@3.4.0", "", { "dependencies": { "@shikijs/types": "3.4.0" } }, "sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ=="], @@ -3483,6 +3489,8 @@ "@httptoolkit/websocket-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "@httptoolkit/websocket-stream/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -3501,8 +3509,6 @@ "@libp2p/websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@libp2p/websockets/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "@macaron-css/babel/@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], "@macaron-css/integration/esbuild": ["esbuild@0.14.54", "", { "optionalDependencies": { "@esbuild/linux-loong64": "0.14.54", "esbuild-android-64": "0.14.54", "esbuild-android-arm64": "0.14.54", "esbuild-darwin-64": "0.14.54", "esbuild-darwin-arm64": "0.14.54", "esbuild-freebsd-64": "0.14.54", "esbuild-freebsd-arm64": "0.14.54", "esbuild-linux-32": "0.14.54", "esbuild-linux-64": "0.14.54", "esbuild-linux-arm": "0.14.54", "esbuild-linux-arm64": "0.14.54", "esbuild-linux-mips64le": "0.14.54", "esbuild-linux-ppc64le": "0.14.54", "esbuild-linux-riscv64": "0.14.54", "esbuild-linux-s390x": "0.14.54", "esbuild-netbsd-64": "0.14.54", "esbuild-openbsd-64": "0.14.54", "esbuild-sunos-64": "0.14.54", "esbuild-windows-32": "0.14.54", "esbuild-windows-64": "0.14.54", "esbuild-windows-arm64": "0.14.54" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA=="], @@ -3519,6 +3525,8 @@ "@multiformats/dns/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "@neondatabase/serverless/@types/node": ["@types/node@22.18.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ=="], + "@nestri/functions/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250522.0", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="], "@nestri/functions/@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="], @@ -3721,6 +3729,8 @@ "@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.33.0", "", {}, "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w=="], + "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + "@opentelemetry/instrumentation-pino/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], "@opentelemetry/instrumentation-pino/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="], @@ -3835,6 +3845,8 @@ "@rocicorp/zero/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + "@rocicorp/zero/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "@rollup/pluginutils/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -3859,6 +3871,10 @@ "@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "@types/pg/@types/node": ["@types/node@22.18.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ=="], + + "@types/pg-pool/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + "@vanilla-extract/css/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@vanilla-extract/integration/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -3983,8 +3999,6 @@ "it-ws/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "it-ws/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "markdown-it/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -4003,6 +4017,8 @@ "mqtt/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "mqtt/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "ofetch/destr": ["destr@2.0.3", "", {}, "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ=="], @@ -4443,6 +4459,8 @@ "@multiformats/dns/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "@neondatabase/serverless/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@nestri/functions/@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], "@openauthjs/solid/@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], @@ -4535,6 +4553,8 @@ "@rocicorp/zero/@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw=="], + "@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@vanilla-extract/integration/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "@vanilla-extract/integration/find-up/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], diff --git a/cloud/packages/core/package.json b/cloud/packages/core/package.json index ab123ce2..27415d79 100644 --- a/cloud/packages/core/package.json +++ b/cloud/packages/core/package.json @@ -29,6 +29,7 @@ "dependencies": { "@aws-sdk/client-iot-data-plane": "^3.758.0", "@aws-sdk/client-sesv2": "^3.753.0", + "@neondatabase/serverless": "^1.0.1", "@openauthjs/openauth": "*", "@openauthjs/openevent": "^0.0.27", "@polar-sh/sdk": "^0.26.1", @@ -44,6 +45,7 @@ "sanitize-html": "^2.16.0", "sharp": "^0.34.1", "steam-session": "*", + "ws": "^8.18.3", "xml2js": "^0.6.2" } } \ No newline at end of file diff --git a/cloud/packages/core/src/context.ts b/cloud/packages/core/src/context.ts index d19af4cc..cfecf02f 100644 --- a/cloud/packages/core/src/context.ts +++ b/cloud/packages/core/src/context.ts @@ -1,17 +1,21 @@ import { AsyncLocalStorage } from "node:async_hooks"; -export function createContext() { - const storage = new AsyncLocalStorage(); - return { - use() { - const result = storage.getStore(); - if (!result) { - throw new Error("No context available"); - } - return result; - }, - provide(value: T, fn: () => R) { - return storage.run(value, fn); - }, - }; -} \ No newline at end of file +export namespace Context { + export class NotFound extends Error {} + + export function create() { + const storage = new AsyncLocalStorage(); + return { + use() { + const result = storage.getStore(); + if (!result) { + throw new NotFound(); + } + return result; + }, + provide(value: T, fn: () => R) { + return storage.run(value, fn); + }, + }; + } +} diff --git a/cloud/packages/core/src/drizzle/index.ts b/cloud/packages/core/src/drizzle/index.ts index 48e3901e..9fbddbfc 100644 --- a/cloud/packages/core/src/drizzle/index.ts +++ b/cloud/packages/core/src/drizzle/index.ts @@ -1,16 +1,102 @@ +import ws from "ws"; import { Resource } from "sst"; -import postgres from "postgres"; -import { drizzle } from "drizzle-orm/postgres-js"; +import { memo } from "../utils"; +import { Context } from "../context"; +import { ExtractTablesWithRelations } from "drizzle-orm"; +import { Pool, neonConfig } from "@neondatabase/serverless"; +import { PgTransaction, PgTransactionConfig } from "drizzle-orm/pg-core"; +import { NeonQueryResultHKT, drizzle } from "drizzle-orm/neon-serverless"; -const client = postgres({ - idle_timeout: 30000, - connect_timeout: 30000, - host: Resource.Database.host, - database: Resource.Database.database, - user: Resource.Database.username, - password: Resource.Database.password, - port: Resource.Database.port, - max: parseInt(process.env.POSTGRES_POOL_MAX || "1"), -}); +neonConfig.webSocketConstructor = ws; -export const db = drizzle(client, {}); \ No newline at end of file +export namespace Database { + function addPoolerSuffix(original: string): string { + const firstDotIndex = original.indexOf("."); + if (firstDotIndex === -1) return original + "-pooler"; + return ( + original.slice(0, firstDotIndex) + + "-pooler" + + original.slice(firstDotIndex) + ); + } + + const client = memo(() => { + const dbHost = addPoolerSuffix(Resource.Database.host); + const pool = new Pool({ + connectionString: `postgres://${Resource.Database.user}:${Resource.Database.password}@${dbHost}/${Resource.Database.name}?sslmode=require`, + }); + const db = drizzle(pool); + return db; + }); + + export type Transaction = PgTransaction< + NeonQueryResultHKT, + Record, + ExtractTablesWithRelations> + >; + + export type TxOrDb = Transaction | ReturnType; + + const TransactionContext = Context.create<{ + tx: TxOrDb; + effects: (() => void | Promise)[]; + }>(); + + export async function use(callback: (trx: TxOrDb) => Promise) { + try { + const { tx } = TransactionContext.use(); + return tx.transaction(callback); + } catch (err) { + if (err instanceof Context.NotFound) { + const effects: (() => void | Promise)[] = []; + const result = await TransactionContext.provide( + { + effects, + tx: client(), + }, + () => callback(client()), + ); + await Promise.all(effects.map((x) => x())); + return result; + } + throw err; + } + } + + export async function fn( + callback: (input: Input, trx: TxOrDb) => Promise, + ) { + return (input: Input) => use(async (tx) => callback(input, tx)); + } + + export async function effect(effect: () => any | Promise) { + try { + const { effects } = TransactionContext.use(); + effects.push(effect); + } catch { + await effect(); + } + } + + export async function transaction( + callback: (tx: TxOrDb) => Promise, + config?: PgTransactionConfig, + ) { + try { + const { tx } = TransactionContext.use(); + return callback(tx); + } catch (err) { + if (err instanceof Context.NotFound) { + const effects: (() => void | Promise)[] = []; + const result = await client().transaction(async (tx) => { + return TransactionContext.provide({ tx, effects }, () => + callback(tx), + ); + }, config); + await Promise.all(effects.map((x) => x())); + return result; + } + throw err; + } + } +} diff --git a/cloud/packages/core/src/drizzle/transaction.ts b/cloud/packages/core/src/drizzle/transaction.ts deleted file mode 100644 index 6a5cbb69..00000000 --- a/cloud/packages/core/src/drizzle/transaction.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { db } from "."; -import { - PgTransaction, - PgTransactionConfig -} from "drizzle-orm/pg-core"; -import { - PostgresJsQueryResultHKT -} from "drizzle-orm/postgres-js"; -import { ExtractTablesWithRelations } from "drizzle-orm"; -import { createContext } from "../context"; - -export type Transaction = PgTransaction< - PostgresJsQueryResultHKT, - Record, - ExtractTablesWithRelations> ->; - -type TxOrDb = Transaction | typeof db; - -const TransactionContext = createContext<{ - tx: Transaction; - effects: (() => void | Promise)[]; -}>(); - -export async function useTransaction(callback: (trx: TxOrDb) => Promise) { - try { - const { tx } = TransactionContext.use(); - return callback(tx); - } catch { - return callback(db); - } -} - -export async function afterTx(effect: () => any | Promise) { - try { - const { effects } = TransactionContext.use(); - effects.push(effect); - } catch { - await effect(); - } -} - -export async function createTransaction( - callback: (tx: Transaction) => Promise, - isolationLevel?: PgTransactionConfig["isolationLevel"], -): Promise { - try { - const { tx } = TransactionContext.use(); - return callback(tx); - } catch { - const effects: (() => void | Promise)[] = []; - const result = await db.transaction( - async (tx) => { - return TransactionContext.provide({ tx, effects }, () => callback(tx)); - }, - { - isolationLevel: isolationLevel || "read committed", - }, - ); - await Promise.all(effects.map((x) => x())); - return result as T; - } -} \ No newline at end of file diff --git a/cloud/packages/core/src/polar/index.ts b/cloud/packages/core/src/polar/index.ts index cbd5f521..c6a8b11d 100644 --- a/cloud/packages/core/src/polar/index.ts +++ b/cloud/packages/core/src/polar/index.ts @@ -1,15 +1,17 @@ import { z } from "zod"; -import { fn } from "../utils"; import { Resource } from "sst"; +import { fn, memo } from "../utils"; import { Polar as PolarSdk } from "@polar-sh/sdk"; import { validateEvent } from "@polar-sh/sdk/webhooks"; export namespace Polar { - export const client = () => - new PolarSdk({ - accessToken: Resource.POLAR_API_KEY.value, - server: Resource.App.stage !== "production" ? "sandbox" : "production", - }); + export const client = memo( + () => + new PolarSdk({ + accessToken: Resource.POLAR_API_KEY.value, + server: Resource.App.stage !== "production" ? "sandbox" : "production", + }), + ); export const fromUserEmail = fn(z.string().min(1), async (email) => { try { diff --git a/cloud/packages/core/src/user/index.ts b/cloud/packages/core/src/user/index.ts index 4b86c3cf..27066be1 100644 --- a/cloud/packages/core/src/user/index.ts +++ b/cloud/packages/core/src/user/index.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { Common } from "../common"; +import { Database } from "../drizzle"; import { createEvent } from "../event"; import { Polar } from "../polar/index"; import { createID, fn } from "../utils"; @@ -7,180 +8,165 @@ import { userTable } from "./user.sql"; import { Examples } from "../examples"; import { and, eq, isNull, asc } from "drizzle-orm"; import { ErrorCodes, VisibleError } from "../error"; -import { createTransaction, useTransaction } from "../drizzle/transaction"; export namespace User { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.User.id, - }), - name: z.string().regex(/^[a-zA-Z ]{1,32}$/, "Use a friendly name.").openapi({ - description: "The name of this account", - example: Examples.User.name - }), - polarCustomerID: z.string().nullable().openapi({ - description: "Associated Polar.sh customer identifier", - example: Examples.User.polarCustomerID, - }), - avatarUrl: z.string().url().nullable().openapi({ - description: "The url to the profile picture", - example: Examples.User.avatarUrl - }), - email: z.string().openapi({ - description: "Primary email address for user notifications and authentication", - example: Examples.User.email, - }), - lastLogin: z.date().openapi({ - description: "Timestamp of user's most recent authentication", - example: Examples.User.lastLogin - }) - }) + export const Info = z + .object({ + id: z.string().openapi({ + description: Common.IdDescription, + example: Examples.User.id, + }), + name: z + .string() + .regex(/^[a-zA-Z ]{1,32}$/, "Use a friendly name.") .openapi({ - ref: "User", - description: "User account entity with core identification and authentication details", - example: Examples.User, - }); + description: "The name of this account", + example: Examples.User.name, + }), + polarCustomerID: z.string().nullable().openapi({ + description: "Associated Polar.sh customer identifier", + example: Examples.User.polarCustomerID, + }), + avatarUrl: z.string().url().nullable().openapi({ + description: "The url to the profile picture", + example: Examples.User.avatarUrl, + }), + email: z.string().openapi({ + description: + "Primary email address for user notifications and authentication", + example: Examples.User.email, + }), + lastLogin: z.date().openapi({ + description: "Timestamp of user's most recent authentication", + example: Examples.User.lastLogin, + }), + }) + .openapi({ + ref: "User", + description: + "User account entity with core identification and authentication details", + example: Examples.User, + }); - export type Info = z.infer; + export type Info = z.infer; - export class UserExistsError extends VisibleError { - constructor(username: string) { - super( - "already_exists", - ErrorCodes.Validation.ALREADY_EXISTS, - `A user with this email ${username} already exists` - ); - } + export class UserExistsError extends VisibleError { + constructor(username: string) { + super( + "already_exists", + ErrorCodes.Validation.ALREADY_EXISTS, + `A user with this email ${username} already exists`, + ); } + } - export const Events = { - Created: createEvent( - "user.created", - z.object({ - userID: Info.shape.id, - }), - ), - }; + export const Events = { + Created: createEvent( + "user.created", + z.object({ + userID: Info.shape.id, + }), + ), + }; - export const create = fn( - Info - .omit({ - lastLogin: true, - polarCustomerID: true, - }).partial({ - avatarUrl: true, - id: true - }), - async (input) => { - const userID = createID("user") + export const create = fn( + Info.omit({ + lastLogin: true, + polarCustomerID: true, + }).partial({ + avatarUrl: true, + id: true, + }), + async (input) => { + const userID = createID("user"); - const customer = await Polar.fromUserEmail(input.email) + const customer = await Polar.fromUserEmail(input.email); - const id = input.id ?? userID; + const id = input.id ?? userID; - await createTransaction(async (tx) => { - const result = await tx - .insert(userTable) - .values({ - id, - avatarUrl: input.avatarUrl, - email: input.email, - name: input.name, - polarCustomerID: customer?.id, - lastLogin: Common.utc() - }) - .onConflictDoNothing({ - target: [userTable.email] - }) - - if (result.count === 0) { - throw new UserExistsError(input.email) - } - }) - - return id; - }) - - export const fromEmail = fn( - Info.shape.email.min(1), - async (email) => - useTransaction(async (tx) => - tx - .select() - .from(userTable) - .where( - and( - eq(userTable.email, email), - isNull(userTable.timeDeleted) - ) - ) - .orderBy(asc(userTable.timeCreated)) - .execute() - .then(rows => rows.map(serialize).at(0)) - ) - ) - - export const fromID = fn( - Info.shape.id.min(1), - (id) => - useTransaction(async (tx) => - tx - .select() - .from(userTable) - .where( - and( - eq(userTable.id, id), - isNull(userTable.timeDeleted) - ) - ) - .orderBy(asc(userTable.timeCreated)) - .execute() - .then(rows => rows.map(serialize).at(0)) - ), - ) - - export const remove = fn( - Info.shape.id.min(1), - (id) => - useTransaction(async (tx) => { - await tx - .update(userTable) - .set({ - timeDeleted: Common.utc(), - }) - .where(and(eq(userTable.id, id))) - .execute(); - return id; - }), - ); - - export const acknowledgeLogin = fn( - Info.shape.id, - (id) => - useTransaction(async (tx) => - tx - .update(userTable) - .set({ - lastLogin: Common.utc(), - }) - .where(and(eq(userTable.id, id))) - .execute() - - ), - ) - - export function serialize( - input: typeof userTable.$inferSelect - ): z.infer { - return { - id: input.id, - name: input.name, - email: input.email, + await Database.transaction(async (tx) => { + const result = await tx + .insert(userTable) + .values({ + id, avatarUrl: input.avatarUrl, - lastLogin: input.lastLogin, - polarCustomerID: input.polarCustomerID, + email: input.email, + name: input.name, + polarCustomerID: customer?.id, + lastLogin: Common.utc(), + }) + .onConflictDoNothing({ + target: [userTable.email], + }); + + if (result.rowCount === 0) { + throw new UserExistsError(input.email); } - } -} \ No newline at end of file + }); + + return id; + }, + ); + + export const fromEmail = fn(Info.shape.email.min(1), async (email) => + Database.transaction(async (tx) => + tx + .select() + .from(userTable) + .where(and(eq(userTable.email, email), isNull(userTable.timeDeleted))) + .orderBy(asc(userTable.timeCreated)) + .execute() + .then((rows) => rows.map(serialize).at(0)), + ), + ); + + export const fromID = fn(Info.shape.id.min(1), (id) => + Database.transaction(async (tx) => + tx + .select() + .from(userTable) + .where(and(eq(userTable.id, id), isNull(userTable.timeDeleted))) + .orderBy(asc(userTable.timeCreated)) + .execute() + .then((rows) => rows.map(serialize).at(0)), + ), + ); + + export const remove = fn(Info.shape.id.min(1), (id) => + Database.transaction(async (tx) => { + await tx + .update(userTable) + .set({ + timeDeleted: Common.utc(), + }) + .where(and(eq(userTable.id, id))) + .execute(); + return id; + }), + ); + + export const acknowledgeLogin = fn(Info.shape.id, (id) => + Database.transaction(async (tx) => + tx + .update(userTable) + .set({ + lastLogin: Common.utc(), + }) + .where(and(eq(userTable.id, id))) + .execute(), + ), + ); + + export function serialize( + input: typeof userTable.$inferSelect, + ): z.infer { + return { + id: input.id, + name: input.name, + email: input.email, + avatarUrl: input.avatarUrl, + lastLogin: input.lastLogin, + polarCustomerID: input.polarCustomerID, + }; + } +} diff --git a/cloud/packages/core/src/utils/index.ts b/cloud/packages/core/src/utils/index.ts index 02816fb5..22fe08e8 100644 --- a/cloud/packages/core/src/utils/index.ts +++ b/cloud/packages/core/src/utils/index.ts @@ -1,5 +1,6 @@ -export * from "./id" -export * from "./fn" -export * from "./log" -export * from "./invite" -export * from "./helper" \ No newline at end of file +export * from "./id"; +export * from "./fn"; +export * from "./log"; +export * from "./invite"; +export * from "./helper"; +export * from "./memo"; diff --git a/cloud/packages/core/src/utils/memo.ts b/cloud/packages/core/src/utils/memo.ts new file mode 100644 index 00000000..6c29e892 --- /dev/null +++ b/cloud/packages/core/src/utils/memo.ts @@ -0,0 +1,18 @@ +export function memo(fn: () => T, cleanup?: (input: T) => Promise) { + let value: T | undefined; + let loaded = false; + + const result = (): T => { + if (loaded) return value as T; + loaded = true; + value = fn(); + return value as T; + }; + result.reset = async () => { + if (cleanup && value) await cleanup(value); + loaded = false; + value = undefined; + }; + + return result; +} diff --git a/cloud/packages/functions/src/auth/index.ts b/cloud/packages/functions/src/auth/index.ts index c1e08d3f..1f902881 100644 --- a/cloud/packages/functions/src/auth/index.ts +++ b/cloud/packages/functions/src/auth/index.ts @@ -7,6 +7,7 @@ import { DiscordAdapter } from "./adapters"; import { issuer } from "@openauthjs/openauth"; import { User } from "@nestri/core/user/index"; import { patchLogger } from "../utils/patch-logger"; +import type { KVNamespace } from "@cloudflare/workers-types"; import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"; interface Env {