From e7087bc4b1be3558a48a94a77107832dd7239e4c Mon Sep 17 00:00:00 2001 From: Draculabo Date: Wed, 24 Dec 2025 23:26:55 +0800 Subject: [PATCH] feat: migrate gateway to nestjs --- package-lock.json | 1080 ++++++++++++++++- package.json | 11 +- src/ipc/gateway/claudeConverter.ts | 185 --- src/ipc/gateway/converter.ts | 207 ---- src/ipc/gateway/geminiClient.ts | 229 ---- src/ipc/gateway/handlers.ts | 24 +- src/ipc/gateway/server.ts | 267 ---- src/ipc/gateway/tokenManager.ts | 141 --- src/ipc/gateway/types.ts | 86 -- src/main.ts | 17 +- src/server/app.module.ts | 9 + src/server/main.ts | 78 ++ .../modules/proxy/clients/gemini.client.ts | 59 + .../proxy/interfaces/request-interfaces.ts | 141 +++ src/server/modules/proxy/proxy.controller.ts | 90 ++ src/server/modules/proxy/proxy.module.ts | 11 + src/server/modules/proxy/proxy.service.ts | 506 ++++++++ .../modules/proxy/token-manager.service.ts | 202 +++ 18 files changed, 2182 insertions(+), 1161 deletions(-) delete mode 100644 src/ipc/gateway/claudeConverter.ts delete mode 100644 src/ipc/gateway/converter.ts delete mode 100644 src/ipc/gateway/geminiClient.ts delete mode 100644 src/ipc/gateway/server.ts delete mode 100644 src/ipc/gateway/tokenManager.ts delete mode 100644 src/ipc/gateway/types.ts create mode 100644 src/server/app.module.ts create mode 100644 src/server/main.ts create mode 100644 src/server/modules/proxy/clients/gemini.client.ts create mode 100644 src/server/modules/proxy/interfaces/request-interfaces.ts create mode 100644 src/server/modules/proxy/proxy.controller.ts create mode 100644 src/server/modules/proxy/proxy.module.ts create mode 100644 src/server/modules/proxy/proxy.service.ts create mode 100644 src/server/modules/proxy/token-manager.service.ts diff --git a/package-lock.json b/package-lock.json index 330377d..21e7dba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "license": "MIT", "dependencies": { "@icons-pack/react-simple-icons": "^13.8.0", + "@nestjs/common": "^11.1.10", + "@nestjs/core": "^11.1.10", + "@nestjs/platform-fastify": "^11.1.10", "@orpc/client": "^1.10.4", "@orpc/server": "^1.11.3", "@radix-ui/react-avatar": "^1.1.11", @@ -31,11 +34,15 @@ "@tanstack/react-router": "^1.139.3", "@tanstack/react-router-devtools": "^1.135.0", "@types/uuid": "^11.0.0", + "axios": "^1.13.2", "better-sqlite3": "^12.4.6", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "electron-squirrel-startup": "^1.0.1", + "fastify": "^5.6.2", "happy-dom": "^20.0.10", "i18next": "^25.6.3", "i18next-browser-languagedetector": "^8.2.0", @@ -44,6 +51,8 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-i18next": "^16.2.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", "undici": "^7.16.0", @@ -674,6 +683,16 @@ "node": ">=6.9.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@code-inspector/core": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@code-inspector/core/-/core-1.3.3.tgz", @@ -2361,6 +2380,218 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@fastify/cors": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", + "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/formbody": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.2.tgz", + "integrity": "sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-querystring": "^1.1.2", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/middie": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-9.0.3.tgz", + "integrity": "sha512-7OYovKXp9UKYeVMcjcFLMcSpoMkmcZmfnG+eAvtdiatN35W7c+r9y1dRfpA+pfFVNuHGGqI3W+vDTmjvcfLcMA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastify-plugin": "^5.0.0", + "path-to-regexp": "^8.1.0", + "reusify": "^1.0.4" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -2790,6 +3021,15 @@ "node": ">=18" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -2813,6 +3053,112 @@ "node": ">= 12.13.0" } }, + "node_modules/@nestjs/common": { + "version": "11.1.10", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.10.tgz", + "integrity": "sha512-NoBzJFtq1bzHGia5Q5NO1pJNpx530nupbEu/auCWOFCGL5y8Zo8kiG28EXTCDfIhQgregEtn1Cs6H8WSLUC8kg==", + "license": "MIT", + "dependencies": { + "file-type": "21.1.1", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.10", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.10.tgz", + "integrity": "sha512-LYpaacSb8X9dcRpeZxA7Mvi5Aozv11s6028ZNoVKY2j/fyThLd+xrkksg3u+sw7F8mipFaxS/LuVpoHQ/MrACg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-fastify": { + "version": "11.1.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-11.1.10.tgz", + "integrity": "sha512-9k26jaDZkq42SaH5/cCsPShd6XeCAuGFF8U63n2lQJwOA8nBmIIGjWy1RDFy+8qFVW0C0DaDdbUPQR5aL274fw==", + "license": "MIT", + "dependencies": { + "@fastify/cors": "11.2.0", + "@fastify/formbody": "8.0.2", + "@fastify/middie": "9.0.3", + "fast-querystring": "1.1.2", + "fastify": "5.6.2", + "light-my-request": "6.6.0", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@fastify/view": "^10.0.0 || ^11.0.0", + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "@fastify/view": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2880,6 +3226,22 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -3351,6 +3713,12 @@ "@orpc/standard-server": "1.11.3" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -6485,6 +6853,29 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -6754,6 +7145,12 @@ "uuid": "*" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -7391,6 +7788,12 @@ "dev": true, "license": "ISC" }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7829,6 +8232,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -7839,6 +8248,15 @@ "node": ">= 4.0.0" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/author-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", @@ -7865,6 +8283,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-dead-code-elimination": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", @@ -8247,7 +8686,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8384,6 +8822,23 @@ "node": ">=6.0" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -8619,6 +9074,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -8646,6 +9113,15 @@ "dev": true, "license": "MIT" }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -8866,7 +9342,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8989,6 +9464,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -9000,7 +9484,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9088,7 +9571,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -9889,7 +10371,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9899,7 +10380,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9944,7 +10424,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -9957,7 +10436,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10582,11 +11060,16 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -10620,6 +11103,69 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-json-stringify": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.1.1.tgz", + "integrity": "sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "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" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -10627,11 +11173,25 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, "funding": [ { "type": "github", @@ -10644,11 +11204,59 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastify": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.6.2.tgz", + "integrity": "sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@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": "^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" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -10677,6 +11285,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.1.1.tgz", + "integrity": "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -10724,6 +11350,20 @@ "node": ">=8" } }, + "node_modules/find-my-way": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz", + "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10776,6 +11416,26 @@ "node": ">= 12" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -10792,6 +11452,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -10851,7 +11527,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10961,7 +11636,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -11028,7 +11702,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -11220,7 +11893,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11353,7 +12025,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11366,7 +12037,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -11382,7 +12052,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11693,6 +12362,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -12207,6 +12885,15 @@ "dev": true, "license": "ISC" }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -12362,6 +13049,25 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -12541,6 +13247,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.33", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz", + "integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==", + "license": "MIT" + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -12551,6 +13263,43 @@ "immediate": "~3.0.5" } }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -12878,6 +13627,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, "node_modules/load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -13252,7 +14020,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13315,7 +14082,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13325,7 +14091,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -13876,6 +14641,15 @@ ], "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -14205,6 +14979,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -14276,6 +15060,43 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, "node_modules/playwright": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", @@ -14607,6 +15428,22 @@ "dev": true, "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -14657,6 +15494,12 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -14698,6 +15541,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -15027,6 +15876,15 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -15081,6 +15939,12 @@ "node": ">=8" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15139,7 +16003,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15248,6 +16111,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -15262,7 +16134,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -15273,7 +16144,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, "license": "MIT" }, "node_modules/rimraf": { @@ -15377,6 +16247,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -15452,6 +16331,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -15535,6 +16442,22 @@ "dev": true, "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -15617,6 +16540,12 @@ "seroval": "^1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15956,6 +16885,15 @@ "seroval": "^1.0" } }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -16032,6 +16970,15 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -16317,6 +17264,22 @@ "node": ">=0.8.0" } }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -16603,6 +17566,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tiny-each-async": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", @@ -16760,6 +17732,33 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -17058,6 +18057,30 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -17344,6 +18367,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index ee7dd88..2c5eecd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@electron-forge/shared-types": "^7.8.1", "@electron/fuses": "~1.8.0", "@electron/rebuild": "^3.7.1", - "node-addon-api": "^8.3.1", "@eslint/compat": "^1.4.1", "@eslint/js": "^9.39.0", "@playwright/test": "^1.56.1", @@ -63,6 +62,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0", "jsdom": "^27.1.0", + "node-addon-api": "^8.3.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.7.1", "tailwindcss": "^4.1.16", @@ -74,6 +74,9 @@ }, "dependencies": { "@icons-pack/react-simple-icons": "^13.8.0", + "@nestjs/common": "^11.1.10", + "@nestjs/core": "^11.1.10", + "@nestjs/platform-fastify": "^11.1.10", "@orpc/client": "^1.10.4", "@orpc/server": "^1.11.3", "@radix-ui/react-avatar": "^1.1.11", @@ -95,11 +98,15 @@ "@tanstack/react-router": "^1.139.3", "@tanstack/react-router-devtools": "^1.135.0", "@types/uuid": "^11.0.0", + "axios": "^1.13.2", "better-sqlite3": "^12.4.6", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "electron-squirrel-startup": "^1.0.1", + "fastify": "^5.6.2", "happy-dom": "^20.0.10", "i18next": "^25.6.3", "i18next-browser-languagedetector": "^8.2.0", @@ -108,6 +115,8 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-i18next": "^16.2.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", "undici": "^7.16.0", diff --git a/src/ipc/gateway/claudeConverter.ts b/src/ipc/gateway/claudeConverter.ts deleted file mode 100644 index ca3e349..0000000 --- a/src/ipc/gateway/claudeConverter.ts +++ /dev/null @@ -1,185 +0,0 @@ -export enum ResponseType { - None = 0, - Thinking = 1, - Text = 2, - Tool = 3, -} - -export interface StreamEvent { - event: string; - data: string; -} - -export class ClaudeStreamConverter { - public responseIndex: number = 0; - public currentType: ResponseType = ResponseType.None; - public hasContent: boolean = false; - - constructor() {} - - processChunk(jsonChunk: any): StreamEvent[] { - const events: StreamEvent[] = []; - - // 0. Handle Prompt Feedback (Blocked requests) - if (!jsonChunk.candidates && !jsonChunk.choices) { - if (jsonChunk.promptFeedback) { - console.warn( - `(Anthropic) Received promptFeedback (likely blocked): ${JSON.stringify(jsonChunk.promptFeedback)}`, - ); - } - return events; - } - - const choices = jsonChunk.choices; - if (!choices || choices.length === 0) return events; - - const choice = choices[0]; - const delta = choice.delta; - - const deltaContent = delta.content || delta.text || ''; // Support both content and text fields just in case - const isThought = delta.thought === true || !!delta.thinking; // Check for thinking flag or content - const thinkingContent = delta.thinking || ''; - const thoughtSignature = delta.thoughtSignature; - - // If we have content of any kind - if (deltaContent || isThought || thoughtSignature || thinkingContent) { - this.hasContent = true; - } - - // 1. Handle Thinking - // If it is explicitly marked as thought, or has thinking content - if (isThought || thoughtSignature || thinkingContent) { - // Close Text if open - if (this.currentType === ResponseType.Text) { - events.push( - this.createEvent('content_block_stop', { - type: 'content_block_stop', - index: this.responseIndex, - }), - ); - this.responseIndex++; - this.currentType = ResponseType.None; - } - - // Open Thinking if not open - if (this.currentType === ResponseType.None) { - events.push( - this.createEvent('content_block_start', { - type: 'content_block_start', - index: this.responseIndex, - content_block: { type: 'thinking', thinking: '' }, // Anthropic Beta expects 'thinking' block - }), - ); - this.currentType = ResponseType.Thinking; - } - - if (thinkingContent) { - events.push( - this.createEvent('content_block_delta', { - type: 'content_block_delta', - index: this.responseIndex, - delta: { type: 'thinking_delta', thinking: thinkingContent }, - }), - ); - } - } - // 2. Handle Regular Text - else if (deltaContent) { - // Close Thinking if open - if (this.currentType === ResponseType.Thinking) { - events.push( - this.createEvent('content_block_stop', { - type: 'content_block_stop', - index: this.responseIndex, - }), - ); - this.responseIndex++; - this.currentType = ResponseType.None; - } - - // Open Text if not open - if (this.currentType === ResponseType.None) { - events.push( - this.createEvent('content_block_start', { - type: 'content_block_start', - index: this.responseIndex, - content_block: { type: 'text', text: '' }, - }), - ); - this.currentType = ResponseType.Text; - } - - events.push( - this.createEvent('content_block_delta', { - type: 'content_block_delta', - index: this.responseIndex, - delta: { type: 'text_delta', text: deltaContent }, - }), - ); - } - - // 3. Handle Stop Reason - if (choice.finish_reason) { - // Close any open block - if (this.currentType !== ResponseType.None) { - events.push( - this.createEvent('content_block_stop', { - type: 'content_block_stop', - index: this.responseIndex, - }), - ); - this.responseIndex++; - this.currentType = ResponseType.None; - } - - let stopReason = 'end_turn'; - const r = choice.finish_reason; - if (r === 'length' || r === 'MAX_TOKENS') stopReason = 'max_tokens'; - else if (r === 'tool_calls') stopReason = 'tool_use'; - // Safety mappings - else if (r === 'SAFETY' || r === 'RECITATION') stopReason = 'end_turn'; // Fallback - - events.push( - this.createEvent('message_delta', { - type: 'message_delta', - delta: { stop_reason: stopReason, stop_sequence: null }, - usage: { output_tokens: 0 }, - }), - ); - - events.push( - this.createEvent('message_stop', { - type: 'message_stop', - }), - ); - } - - return events; - } - - static createMessageStart(msgId: string, model: string): StreamEvent { - return { - event: 'message_start', - data: JSON.stringify({ - type: 'message_start', - message: { - id: msgId, - type: 'message', - role: 'assistant', - model: model, - content: [], - stop_reason: null, - stop_sequence: null, - usage: { input_tokens: 0, output_tokens: 0 }, - }, - }), - }; - } - - createEvent(name: string, data: any): StreamEvent { - return { - event: name, - data: JSON.stringify(data), - }; - } -} diff --git a/src/ipc/gateway/converter.ts b/src/ipc/gateway/converter.ts deleted file mode 100644 index 85bb390..0000000 --- a/src/ipc/gateway/converter.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { - OpenAIChatRequest, - OpenAIMessage, - GeminiContent, - GeminiPart, - GeminiInlineData, - AnthropicChatRequest, - AnthropicMessage, - AnthropicContent, - ContentPart, -} from './types'; -import { logger } from '../../utils/logger'; - -// Helper to extract text from content -function getTextFromContent(content: string | ContentPart[]): string { - if (typeof content === 'string') return content; - return content - .map((p) => { - if (p.type === 'text') return p.text || ''; - return ''; - }) - .join(''); -} - -function getPreview(content: string | ContentPart[]): string { - const text = getTextFromContent(content); - return text.length > 200 ? text.substring(0, 200) + '...' : text; -} - -export const Converter = { - /** - * Convert OpenAI messages to Gemini contents - */ - convertOpenAIToGeminiContents(messages: OpenAIMessage[]): GeminiContent[] { - const contents: GeminiContent[] = []; - const pendingImages: GeminiInlineData[] = []; - - // Regex for markdown images: ![alt](data:image/png;base64,...) - // Group 1: mime, Group 2: data - const reMarkdown = - /!\[.*?\]\(data:\s*(image\/[a-zA-Z+-]+)\s*;\s*base64\s*,\s*([a-zA-Z0-9+/=\s]+)\)/g; - // Regex for explicit data URLs - const reDataUrl = /data:\s*(image\/[a-zA-Z+-]+)\s*;\s*base64\s*,\s*([a-zA-Z0-9+/=\s]+)/; - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - // logger.info(`Msg[${i}][${msg.role}] content=${getPreview(msg.content)}`); - - const role = msg.role === 'assistant' ? 'model' : msg.role === 'system' ? 'user' : msg.role; - const parts: GeminiPart[] = []; - - // 1. Inject Pending Images from Assistant history to User - if (role === 'user' && pendingImages.length > 0) { - logger.info(`Injecting ${pendingImages.length} pending images to User message`); - while (pendingImages.length > 0) { - const img = pendingImages.shift(); - if (img) parts.push({ inlineData: img }); - } - } - - // 2. Parse Content - if (typeof msg.content === 'string') { - const text = msg.content; - let lastEnd = 0; - let match; - - // Reset regex state - reMarkdown.lastIndex = 0; - - while ((match = reMarkdown.exec(text)) !== null) { - const matchStart = match.index; - const matchEnd = reMarkdown.lastIndex; - - if (matchStart > lastEnd) { - const textPart = text.substring(lastEnd, matchStart); - if (textPart) parts.push({ text: textPart }); - } - - const mimeType = match[1]; - const data = match[2].replace(/\s/g, ''); - const inlineData = { mimeType, data }; - - if (role === 'model') { - pendingImages.push(inlineData); - } else { - parts.push({ inlineData }); - } - lastEnd = matchEnd; - } - - if (lastEnd < text.length) { - const textPart = text.substring(lastEnd); - if (textPart) parts.push({ text: textPart }); - } - } else if (Array.isArray(msg.content)) { - // Array format - for (const part of msg.content) { - if (part.type === 'text') { - if (part.text) parts.push({ text: part.text }); - } else if (part.type === 'image_url' && part.image_url) { - const url = part.image_url.url; - const validMatch = url.match(reDataUrl); - if (validMatch) { - const mimeType = validMatch[1]; - const data = validMatch[2].replace(/\s/g, ''); - const inlineData = { mimeType, data }; - - if (role === 'model') { - pendingImages.push(inlineData); - } else { - parts.push({ inlineData }); - } - } else { - logger.warn(`Ignoring unsupported image URL format`); - } - } - } - } - - // 3. Fallbacks - if (role === 'model' && parts.length === 0 && pendingImages.length > 0) { - // Was likely just an image in assistant history, we moved it to pending. - // But model message cannot be empty? - // Actually the loop above pushes for 'model' into pendingImages ONLY if it matches helper logic. - // If the loops finishes and we have parts, good. - // If empty and we put stuff in pendingImages, we need a placeholder? - parts.push({ text: '[Image Generated]' }); - } - - if (parts.length === 0) { - parts.push({ text: '' }); - } - - contents.push({ role, parts }); - } - - // Merge User Messages - return this.mergeConsecutiveUserMessages(contents); - }, - - convertAnthropicToGeminiContents( - request: AnthropicChatRequest, - signatureMap?: Map, - ): GeminiContent[] { - const contents: GeminiContent[] = []; - - for (const msg of request.messages) { - const role = msg.role === 'assistant' ? 'model' : 'user'; - const parts: GeminiPart[] = []; - - const msgContent = Array.isArray(msg.content) - ? msg.content - : [{ type: 'text', text: msg.content } as AnthropicContent]; - - for (const content of msgContent) { - if (content.type === 'text' && content.text) { - parts.push({ text: content.text }); - } else if (content.type === 'image' && content.source && content.source.type === 'base64') { - parts.push({ - inlineData: { - mimeType: content.source.media_type, - data: content.source.data, - }, - }); - } else if (content.type === 'thinking') { - // Ignore thinking input - } - } - - // Thought Signature Injection (for Assistant) - if (role === 'model' && signatureMap) { - const latest = signatureMap.get('latest'); - if (latest) { - parts.push({ thoughtSignature: latest }); - } - } - - contents.push({ role, parts }); - } - - return this.mergeConsecutiveUserMessages(contents); - }, - - mergeConsecutiveUserMessages(contents: GeminiContent[]): GeminiContent[] { - let i = 1; - while (i < contents.length) { - if (contents[i].role === 'user' && contents[i - 1].role === 'user') { - const prev = contents[i - 1]; - const curr = contents[i]; - - // Add separator if needed - const prevLast = prev.parts[prev.parts.length - 1]; - const currFirst = curr.parts[0]; - - if (prevLast && prevLast.text !== undefined && currFirst && currFirst.text !== undefined) { - prev.parts.push({ text: '\n\n' }); - } - - prev.parts.push(...curr.parts); - contents.splice(i, 1); - } else { - i++; - } - } - return contents; - }, -}; diff --git a/src/ipc/gateway/geminiClient.ts b/src/ipc/gateway/geminiClient.ts deleted file mode 100644 index 7792dcd..0000000 --- a/src/ipc/gateway/geminiClient.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { OpenAIChatRequest, AnthropicChatRequest, GeminiContent } from './types'; -import { Converter } from './converter'; -import { logger } from '../../utils/logger'; -import { ProxyAgent } from 'undici'; -import { UpstreamProxyConfig } from '../../types/config'; - -// Type definitions for fetch (since we are in Node) -// Electron main process has 'fetch' globally available in newer versions (Node 18+), -// but we might need to handle types. -declare const fetch: any; - -export class GeminiClient { - private timeoutMs: number; - private upstreamProxy?: UpstreamProxyConfig; - - constructor(timeoutSeconds: number = 60, upstreamProxy?: UpstreamProxyConfig) { - this.timeoutMs = timeoutSeconds * 1000; - this.upstreamProxy = upstreamProxy; - } - - private getFetchOptions() { - if (this.upstreamProxy?.enabled && this.upstreamProxy.url) { - return { - dispatcher: new ProxyAgent(this.upstreamProxy.url), - }; - } - return {}; - } - - async streamGenerateAnthropic( - request: AnthropicChatRequest, - accessToken: string, - projectId: string, - sessionId: string, - signatureMap?: Map, - modelMapping?: Record, - ): Promise { - // Returns a ReadableStream or similar - const url = - 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse'; - - const contents = Converter.convertAnthropicToGeminiContents(request, signatureMap); - - // ... (System prompt handling remains same) - const systemInstruction = request.system - ? { - role: 'user', - parts: [ - { - text: - typeof request.system === 'string' - ? request.system - : JSON.stringify(request.system), - }, - ], - } - : { role: 'user', parts: [{ text: '' }] }; - - const generationConfig: any = { - temperature: request.temperature ?? 1.0, - topP: request.top_p ?? 0.95, - maxOutputTokens: request.max_tokens ?? 16384, - candidateCount: 1, - }; - - if (request.model.includes('sonnet') || request.model.includes('thinking')) { - generationConfig.thinkingConfig = { - includeThoughts: true, - thinkingBudget: 8191, - }; - } - - let upstreamModel = request.model; - - // 1. User Custom Mapping (Fuzzy match) - if (modelMapping) { - for (const [key, value] of Object.entries(modelMapping)) { - if (request.model.includes(key)) { - upstreamModel = value; - break; - } - } - } - - // 2. Built-in Fallback Logic - const lowerName = upstreamModel.toLowerCase(); - if (!upstreamModel.startsWith('gemini')) { - // Only map if not already a gemini model (roughly) - if (lowerName.includes('sonnet') || lowerName.includes('thinking')) { - upstreamModel = 'gemini-3-pro-preview'; - } else if (lowerName.includes('haiku')) { - upstreamModel = 'gemini-2.0-flash-exp'; - } else if (lowerName.includes('opus')) { - upstreamModel = 'gemini-3-pro-preview'; - } else if (lowerName.includes('claude')) { - upstreamModel = 'gemini-2.5-flash-thinking'; - } else if (lowerName === 'gemini-3-pro-high' || lowerName === 'gemini-3-pro-low') { - upstreamModel = 'gemini-3-pro-preview'; - } else if (lowerName === 'gemini-3-flash') { - upstreamModel = 'gemini-3-flash-preview'; - } - } - - const requestBody = { - project: projectId, - requestId: uuidv4(), - model: upstreamModel, - userAgent: 'antigravity', - request: { - contents, - systemInstruction, - generationConfig, - sessionId, - }, - }; - - logger.debug( - `(Gateway) Anthropic Request: ${request.model} -> ${upstreamModel} | Proj: ${projectId}`, - ); - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), this.timeoutMs); - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - Host: 'daily-cloudcode-pa.sandbox.googleapis.com', - 'User-Agent': 'claude-cli/1.0.83 (external, cli)', - 'X-App': 'cli', - 'Anthropic-Beta': 'claude-code-20250219,interleaved-thinking-2025-05-14', - 'X-Stainless-Lang': 'js', - }, - body: JSON.stringify(requestBody), - signal: controller.signal, - ...this.getFetchOptions(), - }); - clearTimeout(timeout); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Upstream Error ${response.status}: ${text}`); - } - - return response.body; // Node.js Readable Web Stream - } catch (e) { - clearTimeout(timeout); - throw e; - } - } - - async generate( - request: OpenAIChatRequest, - accessToken: string, - projectId: string, - sessionId: string, - ): Promise { - const url = 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent'; - - // Split System - const systemMessages = request.messages.filter((m) => m.role === 'system'); - const chatMessages = request.messages.filter((m) => m.role !== 'system'); - - const systemText = systemMessages - .map((m) => { - if (typeof m.content === 'string') return m.content; - return ''; - }) - .join('\n'); - - const contents = Converter.convertOpenAIToGeminiContents(chatMessages); - - const generationConfig = { - temperature: request.temperature ?? 1.0, - topP: request.top_p ?? 0.95, - maxOutputTokens: request.max_tokens ?? 8096, - candidateCount: 1, - }; - - const requestBody = { - project: projectId, - requestId: uuidv4(), - model: request.model, - userAgent: 'antigravity', - request: { - contents, - systemInstruction: { - role: 'user', - parts: [{ text: systemText }], - }, - generationConfig, - toolConfig: { functionCallingConfig: { mode: 'VALIDATED' } }, - sessionId, - }, - }; - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), this.timeoutMs); - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - Host: 'daily-cloudcode-pa.sandbox.googleapis.com', - 'User-Agent': 'antigravity/1.11.3 windows/amd64', - }, - body: JSON.stringify(requestBody), - signal: controller.signal, - ...this.getFetchOptions(), - }); - clearTimeout(timeout); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Upstream Error ${response.status}: ${text}`); - } - - return await response.json(); - } catch (e) { - clearTimeout(timeout); - throw e; - } - } -} diff --git a/src/ipc/gateway/handlers.ts b/src/ipc/gateway/handlers.ts index 827042e..c97f87b 100644 --- a/src/ipc/gateway/handlers.ts +++ b/src/ipc/gateway/handlers.ts @@ -1,21 +1,20 @@ /** * Gateway IPC Handlers - * Provides ORPC handlers for controlling the API Gateway service + * Provides ORPC handlers for controlling the API Gateway service (NestJS version) */ -import { gatewayInstance } from './server'; +import { bootstrapNestServer, stopNestServer, getNestServerStatus } from '../../server/main'; import { ConfigManager } from '../config/manager'; import { v4 as uuidv4 } from 'uuid'; /** - * Start the gateway server + * Start the gateway server (NestJS) */ export const startGateway = async (port: number): Promise => { try { - if (gatewayInstance.isRunning()) { - await gatewayInstance.stop(); - } - await gatewayInstance.start(port); - return true; + // Stop if already running + await stopNestServer(); + // Start NestJS server + return await bootstrapNestServer(port); } catch (e) { console.error('Failed to start gateway:', e); return false; @@ -23,12 +22,11 @@ export const startGateway = async (port: number): Promise => { }; /** - * Stop the gateway server + * Stop the gateway server (NestJS) */ export const stopGateway = async (): Promise => { try { - await gatewayInstance.stop(); - return true; + return await stopNestServer(); } catch (e) { console.error('Failed to stop gateway:', e); return false; @@ -36,10 +34,10 @@ export const stopGateway = async (): Promise => { }; /** - * Get gateway status + * Get gateway status (NestJS) */ export const getGatewayStatus = async () => { - return gatewayInstance.getStatus(); + return getNestServerStatus(); }; /** diff --git a/src/ipc/gateway/server.ts b/src/ipc/gateway/server.ts deleted file mode 100644 index a5b8f8f..0000000 --- a/src/ipc/gateway/server.ts +++ /dev/null @@ -1,267 +0,0 @@ -import http from 'http'; -import { TokenManager } from './tokenManager'; -import { GeminiClient } from './geminiClient'; -import { ClaudeStreamConverter } from './claudeConverter'; -import { logger } from '../../utils/logger'; -import { Readable } from 'stream'; -import { ConfigManager } from '../config/manager'; -import { v4 as uuidv4 } from 'uuid'; - -const PORT = 3000; // Default, make configurable? - -export class GatewayServer { - private server: http.Server | null = null; - private tokenManager: TokenManager; - private client: GeminiClient; - private signatureMap: Map = new Map(); - private currentPort: number = 0; - - constructor() { - this.tokenManager = new TokenManager(); - this.client = new GeminiClient(60); - } - - async start(port: number = PORT) { - // Load config and accounts - const config = ConfigManager.loadConfig(); - await this.tokenManager.loadAccounts(); - logger.info(`Gateway: Loaded ${await this.tokenManager.loadAccounts()} accounts`); - - // Initialize Client with config - // Default to 60s if not set, pass upstream proxy config - this.client = new GeminiClient( - config.proxy?.request_timeout || 60, - config.proxy?.upstream_proxy, - ); - - this.server = http.createServer(async (req, res) => { - // CORS - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', '*'); - - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - - const url = new URL(req.url || '/', `http://${req.headers.host}`); - - try { - if (url.pathname === '/v1/messages' && req.method === 'POST') { - await this.handleAnthropicMessages(req, res); - } else if (url.pathname === '/v1/chat/completions' && req.method === 'POST') { - // await this.handleChatCompletions(req, res); - res.writeHead(501); - res.end('Not Implemented Yet'); - } else if (url.pathname === '/healthz') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok' })); - } else { - res.writeHead(404); - res.end('Not Found'); - } - } catch (e) { - logger.error('Gateway: Request Error', e); - if (!res.headersSent) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: String(e) })); - } - } - }); - - this.server.on('error', (e: any) => { - if (e.code === 'EADDRINUSE') { - logger.error(`Gateway: Port ${port} is already in use.`); - } else { - logger.error('Gateway: Server error', e); - } - }); - - this.server.listen(port, () => { - this.currentPort = port; - logger.info(`Gateway: Server listening on http://127.0.0.1:${port}`); - }); - } - - private async handleAnthropicMessages(req: http.IncomingMessage, res: http.ServerResponse) { - const body = await this.readBody(req); - const request = JSON.parse(body); - - logger.info(`(Anthropic) Request ${request.model}`); - - // Retry Loop - const maxRetries = 3; // or tokenManager count - let attempts = 0; - - while (attempts < maxRetries) { - attempts++; - const token = await this.tokenManager.getToken(); - - if (!token) { - res.writeHead(503, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ error: { type: 'overloaded_error', message: 'No accounts available' } }), - ); - return; - } - - logger.info(`(Anthropic) Using account ${token.email} (Attempt ${attempts})`); - - try { - const config = ConfigManager.loadConfig(); - const mapping = config.proxy?.anthropic_mapping || {}; - - const stream = await this.client.streamGenerateAnthropic( - request, - token.access_token, - token.project_id || 'unknown-project', - token.session_id, - this.signatureMap, - mapping, - ); - - // Handle SSE - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }); - - const msgId = `msg_${uuidv4()}`; - const startEvent = ClaudeStreamConverter.createMessageStart(msgId, request.model); - res.write(`event: ${startEvent.event}\ndata: ${startEvent.data}\n\n`); - - const converter = new ClaudeStreamConverter(); - - // Read the stream - // Node fetch response.body is a ReadableStream (web stream) or IncomingMessage (Node stream) depending on fetch polyfill - // Assuming it's an async iterable or we can get a reader. - // In Node 18+ native fetch, body is a Web stream. - - for await (const chunk of stream) { - // chunk is Buffer or Uint8Array - const text = Buffer.from(chunk).toString('utf-8'); - const lines = text.split('\n'); - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') continue; - - try { - const json = JSON.parse(data); - // Logic from server.rs: process_chunk -> get events - const events = converter.processChunk(json); - for (const event of events) { - res.write(`event: ${event.event}\ndata: ${event.data}\n\n`); - } - } catch (e) { - logger.warn('Gateway: Failed to parse Gemini chunk', e); - } - } - } - } - - res.end(); - return; // Success - } catch (e: any) { - logger.error(`(Anthropic) Request failed: ${e.message}`); - // Check retry - if (this.shouldRetry(e.message) && attempts < maxRetries) { - logger.warn(`(Anthropic) Retrying...`); - continue; - } - - // Final error - if (!res.headersSent) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { type: 'api_error', message: e.message } })); - } - return; - } - } - } - - private shouldRetry(errorMsg: string): boolean { - // Simple heuristic - if ( - errorMsg.includes('429') || - errorMsg.includes('503') || - errorMsg.includes('500') || - errorMsg.includes('403') - ) - return true; - return false; - } - - private readBody(req: http.IncomingMessage): Promise { - return new Promise((resolve, reject) => { - let body = ''; - req.on('data', (chunk) => (body += chunk)); - req.on('end', () => resolve(body)); - req.on('error', reject); - }); - } - - // ===== Lifecycle & Status Methods ===== - - /** - * Stop the gateway server - */ - stop(): Promise { - return new Promise((resolve, reject) => { - if (!this.server) { - resolve(); - return; - } - this.server.close((err) => { - if (err) { - logger.error('Gateway: Failed to stop server', err); - reject(err); - } else { - logger.info('Gateway: Server stopped'); - this.server = null; - this.currentPort = 0; - resolve(); - } - }); - }); - } - - /** - * Check if the server is running - */ - isRunning(): boolean { - return this.server !== null && this.server.listening; - } - - /** - * Get current gateway status - */ - async getStatus(): Promise<{ - running: boolean; - port: number; - base_url: string; - active_accounts: number; - }> { - const accountCount = await this.getAccountCount(); - return { - running: this.isRunning(), - port: this.currentPort, - base_url: this.isRunning() ? `http://localhost:${this.currentPort}` : '', - active_accounts: accountCount, - }; - } - - /** - * Get the number of loaded accounts - */ - async getAccountCount(): Promise { - return this.tokenManager.loadAccounts(); - } -} - -// Singleton instance for global access -export const gatewayInstance = new GatewayServer(); diff --git a/src/ipc/gateway/tokenManager.ts b/src/ipc/gateway/tokenManager.ts deleted file mode 100644 index be05167..0000000 --- a/src/ipc/gateway/tokenManager.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { ProxyToken } from './types'; -import { logger } from '../../utils/logger'; -import { GoogleAPIService } from '../../services/GoogleAPIService'; -import { CloudAccountRepo } from '../database/cloudHandler'; -import { CloudAccount } from '../../types/cloudAccount'; - -export class TokenManager { - private tokens: Map = new Map(); - private currentIndex: number = 0; - - constructor() {} - - async loadAccounts(): Promise { - // Load from SQLite DB - // Fix: Use getAccounts() instead of list() - const accounts = await CloudAccountRepo.getAccounts(); - let count = 0; - - for (const account of accounts) { - try { - const token = this.convertAccountToToken(account); - if (token) { - this.tokens.set(token.account_id, token); - count++; - } - } catch (e) { - logger.warn(`Gateway: Failed to load account ${account.email}`, e); - } - } - return count; - } - - private convertAccountToToken(account: CloudAccount): ProxyToken | null { - // Fix: account.token instead of account.token_data - if (!account.token) return null; - - return { - account_id: account.id, - email: account.email, - access_token: account.token.access_token, - refresh_token: account.token.refresh_token, - expires_in: account.token.expires_in, - timestamp: account.token.expiry_timestamp, - account_path: '', // Not used with DB - project_id: account.token.project_id || undefined, - session_id: account.token.session_id || this.generateSessionId(), - }; - } - - generateSessionId(): string { - const min = 1_000_000_000_000_000_000n; - const max = 9_000_000_000_000_000_000n; - const range = max - min; - const rand = BigInt(Math.floor(Math.random() * Number(range))); - return (-(min + rand)).toString(); - } - - async getToken(): Promise { - if (this.tokens.size === 0) { - await this.loadAccounts(); - } - if (this.tokens.size === 0) return null; - - const keys = Array.from(this.tokens.keys()); - this.currentIndex = (this.currentIndex + 1) % keys.length; - const key = keys[this.currentIndex]; - const token = this.tokens.get(key); - - if (!token) return null; - - const now = Math.floor(Date.now() / 1000); - if (now >= token.timestamp - 300) { - logger.info(`Gateway: Token for ${token.email} expiring, refreshing...`); - try { - const newTokens = await GoogleAPIService.refreshAccessToken(token.refresh_token); - - // Update Token Object - token.access_token = newTokens.access_token; - token.expires_in = newTokens.expires_in; - token.timestamp = Math.floor(Date.now() / 1000) + newTokens.expires_in; - - // Update DB - await this.saveRefreshedToken(token.account_id, token); - - this.tokens.set(token.account_id, token); - logger.info(`Gateway: Token refreshed for ${token.email}`); - } catch (e) { - logger.error(`Gateway: Failed to refresh token for ${token.email}`, e); - } - } - - if (!token.project_id) { - try { - const mockId = `cloud-code-${Math.floor(Math.random() * 100000)}`; - token.project_id = mockId; - await this.saveProjectId(token.account_id, mockId); - } catch (e) { - logger.warn('Gateway: Failed to resolve project ID', e); - } - } - - return token; - } - - async saveRefreshedToken(accountId: string, token: ProxyToken) { - try { - const acc = await CloudAccountRepo.getAccount(accountId); - if (acc && acc.token) { - // Update token fields - const newToken = { - ...acc.token, - access_token: token.access_token, - expires_in: token.expires_in, - expiry_timestamp: token.timestamp, - }; - - // Use updateToken which expects the whole token object (or partial?) - // CloudHandler.updateToken expects JSON object that it stringifies. - await CloudAccountRepo.updateToken(accountId, newToken); - } - } catch (e) { - logger.error(`Gateway: Failed to save refreshed token to DB`, e); - } - } - - async saveProjectId(accountId: string, projectId: string) { - try { - // CloudAccountRepo.getAccount is async - const acc = await CloudAccountRepo.getAccount(accountId); - if (acc && acc.token) { - const newToken = { - ...acc.token, - project_id: projectId, - }; - await CloudAccountRepo.updateToken(accountId, newToken); - } - } catch (e) { - logger.error(`Gateway: Failed to save project ID to DB`, e); - } - } -} diff --git a/src/ipc/gateway/types.ts b/src/ipc/gateway/types.ts deleted file mode 100644 index faffa42..0000000 --- a/src/ipc/gateway/types.ts +++ /dev/null @@ -1,86 +0,0 @@ -export interface ProxyToken { - account_id: string; - access_token: string; - refresh_token: string; - expires_in: number; - timestamp: number; - email: string; - account_path: string; - project_id?: string; - session_id: string; -} - -// OpenAI Types -export interface OpenAIMessage { - role: string; - content: string | ContentPart[]; - name?: string; -} - -export interface ContentPart { - type: 'text' | 'image_url'; - text?: string; - image_url?: { url: string }; -} - -export interface OpenAIChatRequest { - model: string; - messages: OpenAIMessage[]; - temperature?: number; - top_p?: number; - max_tokens?: number; - stream?: boolean; - size?: string; - quality?: string; - user?: string; - [key: string]: any; // Extra params -} - -// Anthropic Types -export interface AnthropicContent { - type: 'text' | 'image' | 'thinking'; - text?: string; // for text or thinking - thinking?: string; - signature?: string; - source?: { - type: 'base64'; - media_type: string; - data: string; - }; -} - -export interface AnthropicMessage { - role: string; - content: AnthropicContent[] | string; // Can be string in some clients - thought_signature?: string; -} - -export interface AnthropicChatRequest { - model: string; - messages: AnthropicMessage[]; - system?: string | any[]; // System prompt - max_tokens?: number; - metadata?: any; - stop_sequences?: string[]; - stream?: boolean; - temperature?: number; - top_p?: number; - top_k?: number; -} - -// Gemini Types -export interface GeminiInlineData { - mimeType: string; - data: string; -} - -export interface GeminiPart { - text?: string; - inlineData?: GeminiInlineData; - thoughtSignature?: string; -} - -export interface GeminiContent { - role: string; - parts: GeminiPart[]; -} diff --git a/src/main.ts b/src/main.ts index 3db86ef..dfb2f2e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,7 @@ import { CloudMonitorService } from './services/CloudMonitorService'; // Static Imports to fix Bundle Resolution Errors import { AuthServer } from './ipc/cloud/authServer'; -import { gatewayInstance } from './ipc/gateway/server'; +import { bootstrapNestServer, stopNestServer } from './server/main'; import { initTray, setTrayLanguage } from './ipc/tray/handler'; import { rpcHandler } from './ipc/handler'; import { ConfigManager } from './ipc/config/manager'; @@ -218,12 +218,12 @@ app // Start OAuth Server AuthServer.start(); - // Gateway Server - use singleton, auto-start if enabled + // Gateway Server (NestJS) - auto-start if enabled const config = ConfigManager.loadConfig(); if (config.proxy?.auto_start) { const port = config.proxy?.port || 8045; - gatewayInstance.start(port); - logger.info(`Gateway: Auto-started on port ${port}`); + await bootstrapNestServer(port); + logger.info(`NestJS Proxy: Auto-started on port ${port}`); } const enabled = CloudAccountRepo.getSetting('auto_switch_enabled', false); @@ -235,7 +235,7 @@ app logger.error('Startup: Failed to initialize services', e); } }) - .then(() => { + .then(async () => { logger.info('Step: Startup Complete'); if (globalMainWindow) { initTray(globalMainWindow); @@ -249,9 +249,10 @@ app //osX only app.on('window-all-closed', () => { logger.info('Window all closed event triggered'); - // if (process.platform !== 'darwin') { - // app.quit(); - // } + stopNestServer(); // Stop server + if (process.platform !== 'darwin') { + app.quit(); + } // Keep app running for tray }); diff --git a/src/server/app.module.ts b/src/server/app.module.ts new file mode 100644 index 0000000..0321cae --- /dev/null +++ b/src/server/app.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ProxyModule } from './modules/proxy/proxy.module'; + +@Module({ + imports: [ProxyModule], + controllers: [], + providers: [], +}) +export class AppModule {} diff --git a/src/server/main.ts b/src/server/main.ts new file mode 100644 index 0000000..c374037 --- /dev/null +++ b/src/server/main.ts @@ -0,0 +1,78 @@ +import { NestFactory } from '@nestjs/core'; +import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; +import { AppModule } from './app.module'; +import { logger } from '../utils/logger'; +import { TokenManagerService } from './modules/proxy/token-manager.service'; + +let app: NestFastifyApplication | null = null; +let currentPort: number = 0; + +export async function bootstrapNestServer(port: number = 8045): Promise { + if (app) { + logger.info('NestJS server already running.'); + return true; + } + + try { + app = await NestFactory.create(AppModule, new FastifyAdapter(), { + logger: ['error', 'warn', 'log'], + }); + + // Enable CORS + app.enableCors(); + + await app.listen(port, '0.0.0.0'); + currentPort = port; + logger.info(`NestJS Proxy Server running on http://localhost:${port}`); + return true; + } catch (error) { + logger.error('Failed to start NestJS server', error); + return false; + } +} + +export async function stopNestServer(): Promise { + if (app) { + try { + await app.close(); + app = null; + currentPort = 0; + logger.info('NestJS server stopped.'); + return true; + } catch (e) { + logger.error('Failed to stop NestJS server', e); + return false; + } + } + return true; +} + +export function isNestServerRunning(): boolean { + return app !== null; +} + +export async function getNestServerStatus(): Promise<{ + running: boolean; + port: number; + base_url: string; + active_accounts: number; +}> { + const running = isNestServerRunning(); + let activeAccounts = 0; + + if (app) { + try { + const tokenManager = app.get(TokenManagerService); + activeAccounts = tokenManager.getAccountCount(); + } catch (e) { + // TokenManager might not be available + } + } + + return { + running, + port: currentPort, + base_url: running ? `http://localhost:${currentPort}` : '', + active_accounts: activeAccounts, + }; +} diff --git a/src/server/modules/proxy/clients/gemini.client.ts b/src/server/modules/proxy/clients/gemini.client.ts new file mode 100644 index 0000000..e2bae94 --- /dev/null +++ b/src/server/modules/proxy/clients/gemini.client.ts @@ -0,0 +1,59 @@ +import { Injectable, Logger } from '@nestjs/common'; +import axios from 'axios'; +import { GeminiRequest, GeminiResponse } from '../interfaces/request-interfaces'; + +@Injectable() +export class GeminiClient { + private readonly logger = new Logger(GeminiClient.name); + // Default to v1beta for most features + private readonly baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; + + async streamGenerate(model: string, content: GeminiRequest, accessToken: string): Promise { + const url = `${this.baseUrl}/models/${model}:streamGenerateContent?alt=sse`; + + try { + const response = await axios.post(url, content, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + responseType: 'stream', + timeout: 60000, + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + this.logger.error(`Gemini Stream API Error: ${error.message}`); + throw new Error(error.response?.data?.error?.message || error.message); + } + throw error; + } + } + + async generate( + model: string, + content: GeminiRequest, + accessToken: string, + ): Promise { + const url = `${this.baseUrl}/models/${model}:generateContent`; + + try { + const response = await axios.post(url, content, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + timeout: 60000, // 60s timeout + }); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + this.logger.error( + `Gemini API Error: ${error.message} - ${JSON.stringify(error.response?.data)}`, + ); + throw new Error(error.response?.data?.error?.message || error.message); + } + throw error; + } + } +} diff --git a/src/server/modules/proxy/interfaces/request-interfaces.ts b/src/server/modules/proxy/interfaces/request-interfaces.ts new file mode 100644 index 0000000..6b3e556 --- /dev/null +++ b/src/server/modules/proxy/interfaces/request-interfaces.ts @@ -0,0 +1,141 @@ +export interface OpenAIChatRequest { + model: string; + messages: OpenAIMessage[]; + temperature?: number; + top_p?: number; + max_tokens?: number; + stream?: boolean; + size?: string; + quality?: string; + extra?: Record; +} + +export interface OpenAIMessage { + role: string; + content: string | OpenAIContentPart[]; + name?: string; +} + +export interface OpenAIContentPart { + type: 'text' | 'image_url'; + text?: string; + image_url?: { + url: string; + }; +} + +export interface AnthropicChatRequest { + model: string; + messages: AnthropicMessage[]; + system?: string | AnthropicSystemBlock[]; + max_tokens?: number; + metadata?: any; + stop_sequences?: string[]; + stream?: boolean; + temperature?: number; + top_p?: number; + top_k?: number; +} + +export interface AnthropicMessage { + role: string; + content: string | AnthropicContent[]; +} + +export interface AnthropicSystemBlock { + type: string; + text: string; +} + +export type AnthropicContent = + | { type: 'text'; text: string } + | { type: 'thinking'; thinking: string; signature?: string } + | { type: 'image'; source: AnthropicImageSource }; + +export interface AnthropicImageSource { + type: 'base64'; + media_type: string; + data: string; +} + +export interface GeminiContent { + role: string; + parts: GeminiPart[]; +} + +export interface GeminiPart { + text?: string; + inlineData?: GeminiInlineData; + thoughtSignature?: string; +} + +export interface GeminiInlineData { + mimeType: string; + data: string; +} + +export interface GeminiRequest { + contents: GeminiContent[]; + systemInstruction?: { parts: GeminiPart[] }; + generationConfig?: GeminiGenerationConfig; +} + +export interface GeminiGenerationConfig { + temperature?: number; + maxOutputTokens?: number; + topP?: number; + topK?: number; +} + +export interface GeminiResponse { + candidates?: GeminiCandidate[]; + usageMetadata?: GeminiUsageMetadata; +} + +export interface GeminiCandidate { + content?: GeminiContent; + finishReason?: string; + index?: number; +} + +export interface GeminiUsageMetadata { + promptTokenCount?: number; + candidatesTokenCount?: number; + totalTokenCount?: number; +} + +export interface OpenAIChatResponse { + id: string; + object: string; + created: number; + model: string; + choices: OpenAIChoice[]; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +export interface OpenAIChoice { + index: number; + message: { + role: string; + content: string; + }; + finish_reason: string; +} + +export interface AnthropicChatResponse { + id: string; + type: string; + role: string; + model: string; + content: AnthropicContent[]; + stop_reason: string | null; + stop_sequence: string | null; + usage: { + input_tokens: number; + output_tokens: number; + }; +} diff --git a/src/server/modules/proxy/proxy.controller.ts b/src/server/modules/proxy/proxy.controller.ts new file mode 100644 index 0000000..2019ec5 --- /dev/null +++ b/src/server/modules/proxy/proxy.controller.ts @@ -0,0 +1,90 @@ +import { Controller, Get, Post, Body, Res, HttpStatus } from '@nestjs/common'; +import { FastifyReply } from 'fastify'; +import { ProxyService } from './proxy.service'; +import { Observable } from 'rxjs'; +import { OpenAIChatRequest, AnthropicChatRequest } from './interfaces/request-interfaces'; + +@Controller('v1') +export class ProxyController { + constructor(private readonly proxyService: ProxyService) {} + + @Get('models') + getModels() { + return { + object: 'list', + data: [ + // Gemini Native + { + id: 'gemini-2.5-flash-thinking', + object: 'model', + created: 1734336000, + owned_by: 'google', + }, + { id: 'gemini-2.5-flash', object: 'model', created: 1734336000, owned_by: 'google' }, + { id: 'gemini-2.5-pro', object: 'model', created: 1734336000, owned_by: 'google' }, + { id: 'gemini-3-flash', object: 'model', created: 1734336000, owned_by: 'google' }, + + // Claude Native (Mapped) + { id: 'claude-sonnet-4-5', object: 'model', created: 1734336000, owned_by: 'anthropic' }, + { + id: 'claude-sonnet-4-5-thinking', + object: 'model', + created: 1734336000, + owned_by: 'anthropic', + }, + + // Image Models + { id: 'gemini-3-pro-image', object: 'model', created: 1734336000, owned_by: 'google' }, + ], + }; + } + + @Post('chat/completions') + async chatCompletions(@Body() body: OpenAIChatRequest, @Res() res: FastifyReply) { + try { + const result = await this.proxyService.handleChatCompletions(body); + + if (body.stream && result instanceof Observable) { + res.header('Content-Type', 'text/event-stream'); + res.header('Cache-Control', 'no-cache'); + res.header('Connection', 'keep-alive'); + res.send(result); + } else { + res.status(HttpStatus.OK).send(result); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal Server Error'; + res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ + error: { + message: message, + type: 'server_error', + }, + }); + } + } + + @Post('messages') + async anthropicMessages(@Body() body: AnthropicChatRequest, @Res() res: FastifyReply) { + try { + const result = await this.proxyService.handleAnthropicMessages(body); + + if (body.stream && result instanceof Observable) { + res.header('Content-Type', 'text/event-stream'); + res.header('Cache-Control', 'no-cache'); + res.header('Connection', 'keep-alive'); + res.send(result); + } else { + res.status(HttpStatus.OK).send(result); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal Server Error'; + res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ + type: 'error', + error: { + type: 'api_error', + message: message, + }, + }); + } + } +} diff --git a/src/server/modules/proxy/proxy.module.ts b/src/server/modules/proxy/proxy.module.ts new file mode 100644 index 0000000..c59b759 --- /dev/null +++ b/src/server/modules/proxy/proxy.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ProxyController } from './proxy.controller'; +import { ProxyService } from './proxy.service'; +import { TokenManagerService } from './token-manager.service'; +import { GeminiClient } from './clients/gemini.client'; + +@Module({ + controllers: [ProxyController], + providers: [ProxyService, TokenManagerService, GeminiClient], +}) +export class ProxyModule {} diff --git a/src/server/modules/proxy/proxy.service.ts b/src/server/modules/proxy/proxy.service.ts new file mode 100644 index 0000000..d00f62b --- /dev/null +++ b/src/server/modules/proxy/proxy.service.ts @@ -0,0 +1,506 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { TokenManagerService } from './token-manager.service'; +import { GeminiClient } from './clients/gemini.client'; +import { v4 as uuidv4 } from 'uuid'; +import { Observable } from 'rxjs'; +import { + OpenAIChatRequest, + AnthropicChatRequest, + GeminiContent, + GeminiPart, + AnthropicSystemBlock, + OpenAIContentPart, + GeminiResponse, + GeminiRequest, + AnthropicChatResponse, + OpenAIChatResponse, + GeminiCandidate, +} from './interfaces/request-interfaces'; + +@Injectable() +export class ProxyService { + private readonly logger = new Logger(ProxyService.name); + + constructor( + private readonly tokenManager: TokenManagerService, + private readonly geminiClient: GeminiClient, + ) {} + + // --- Anthropic Handlers --- + + async handleAnthropicMessages( + request: AnthropicChatRequest, + ): Promise> { + const targetModel = this.mapModel(request.model); + this.logger.log( + `Received Anthropic request for model: ${request.model} (Mapped: ${targetModel}, Stream: ${request.stream})`, + ); + + // Retry loop + let lastError: unknown = null; + const maxRetries = 3; + + for (let i = 0; i < maxRetries; i++) { + const token = await this.tokenManager.getNextToken(); + if (!token) { + throw new Error('No available accounts'); + } + + try { + // Convert Anthropic -> Gemini + const geminiRequest = this.convertAnthropicToGemini(request); + + if (request.stream) { + const stream = await this.geminiClient.streamGenerate( + targetModel, + geminiRequest, + token.token.access_token, + ); + return this.processAnthropicStreamResponse(stream, request.model); + } else { + const response = await this.geminiClient.generate( + targetModel, + geminiRequest, + token.token.access_token, + ); + return this.convertGeminiToAnthropicResponse(response, request.model); + } + } catch (error) { + lastError = error; + if (error instanceof Error) { + this.logger.warn(`Anthropic Request failed: ${error.message}`); + if (this.shouldRetry(error.message)) { + this.tokenManager.markAsRateLimited(token.email); + } + } + } + } + throw lastError || new Error('Request failed after retries'); + } + + private convertAnthropicToGemini(request: AnthropicChatRequest): GeminiRequest { + const contents: GeminiContent[] = []; + let systemInstruction: { parts: GeminiPart[] } | undefined = undefined; + + if (request.system) { + let systemText = ''; + if (typeof request.system === 'string') { + systemText = request.system; + } else if (Array.isArray(request.system)) { + systemText = request.system + .filter((b: AnthropicSystemBlock) => b.type === 'text') + .map((b: AnthropicSystemBlock) => b.text) + .join('\n'); + } + + systemInstruction = { + parts: [{ text: systemText }], + }; + } + + // Anthropic 'messages' -> Gemini 'contents' + for (const msg of request.messages) { + const role = msg.role === 'user' ? 'user' : 'model'; + // Handle content arrays or strings + let textPart = ''; + if (Array.isArray(msg.content)) { + textPart = msg.content + .filter((c) => c.type === 'text') + .map((c) => (c as { type: 'text'; text: string }).text) + .join('\n'); + } else { + textPart = msg.content; + } + + contents.push({ + role, + parts: [{ text: textPart }], + }); + } + + return { + contents, + systemInstruction, + generationConfig: { + temperature: request.temperature, + maxOutputTokens: request.max_tokens, + topP: request.top_p, + topK: request.top_k, + }, + // Note: Rust version handles 'pending_images' logic here, but for now we assume simple text/image structure. + // If we need to support image injection from history to user msg, that logic needs to be ported too. + // Current port matches previous 'any' style logic but with types. + }; + } + + private convertGeminiToAnthropicResponse( + geminiResponse: GeminiResponse, + model: string, + ): AnthropicChatResponse { + const candidate = geminiResponse.candidates?.[0]; + const processed = this.processInlineData(candidate); + const parts = processed?.content?.parts || []; + const contentText = parts.map((p) => p.text || '').join(''); + + return { + id: `msg_${uuidv4()}`, + type: 'message', + role: 'assistant', + model: model, + content: [{ type: 'text', text: contentText }], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, + output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0, + }, + }; + } + + private processAnthropicStreamResponse(upstreamStream: any, model: string): Observable { + return new Observable((subscriber) => { + const decoder = new TextDecoder(); + let buffer = ''; + const msgId = `msg_${uuidv4()}`; + + // Send Message Start + subscriber.next( + `event: message_start\ndata: ${JSON.stringify({ + type: 'message_start', + message: { + id: msgId, + type: 'message', + role: 'assistant', + content: [], + model: model, + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 0, output_tokens: 0 }, + }, + })}\n\n`, + ); + + subscriber.next( + `event: content_block_start\ndata: ${JSON.stringify({ + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + })}\n\n`, + ); + + upstreamStream.on('data', (chunk: Buffer) => { + buffer += decoder.decode(chunk, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith('data: ')) continue; + const dataStr = trimmed.slice(6); + if (dataStr === '[DONE]') continue; + + try { + const json = JSON.parse(dataStr); + const candidate = json.candidates?.[0]; + const contentPart = candidate?.content?.parts?.[0]; + const text = contentPart?.text || ''; + + if (text) { + subscriber.next( + `event: content_block_delta\ndata: ${JSON.stringify({ + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: text }, + })}\n\n`, + ); + } + } catch (e) {} + } + }); + + upstreamStream.on('end', () => { + subscriber.next( + `event: content_block_stop\ndata: ${JSON.stringify({ + type: 'content_block_stop', + index: 0, + })}\n\n`, + ); + + subscriber.next( + `event: message_delta\ndata: ${JSON.stringify({ + type: 'message_delta', + delta: { stop_reason: 'end_turn', stop_sequence: null }, + usage: { output_tokens: 0 }, + })}\n\n`, + ); + + subscriber.next( + `event: message_stop\ndata: ${JSON.stringify({ + type: 'message_stop', + })}\n\n`, + ); + + subscriber.complete(); + }); + + upstreamStream.on('error', (err: any) => subscriber.error(err)); + }); + } + + // --- OpenAI / Universal Handlers --- + + async handleChatCompletions( + request: OpenAIChatRequest, + ): Promise> { + const targetModel = this.mapModel(request.model); + this.logger.log( + `Received request for model: ${request.model} (Mapped: ${targetModel}, Stream: ${request.stream})`, + ); + + // Retry loop for account selection + let lastError: unknown = null; + const maxRetries = 3; + + for (let i = 0; i < maxRetries; i++) { + // 1. Get Token + const token = await this.tokenManager.getNextToken(); + if (!token) { + throw new Error('No available accounts (all exhausted or rate limited)'); + } + + try { + // 2. Convert Request + const geminiRequest = this.convertOpenAIToGemini(request); + + // 3. Handle Stream vs Non-Stream + if (request.stream) { + const stream = await this.geminiClient.streamGenerate( + targetModel, + geminiRequest, + token.token.access_token, + ); + return this.processStreamResponse(stream, request.model); + } else { + const response = await this.geminiClient.generate( + targetModel, + geminiRequest, + token.token.access_token, + ); + return this.convertGeminiToOpenAIResponse(response, request.model); + } + } catch (error) { + lastError = error; + if (error instanceof Error) { + this.logger.warn(`Request failed with account ${token.email}: ${error.message}`); + + // Check for rate limit / auth errors + if (this.shouldRetry(error.message)) { + this.tokenManager.markAsRateLimited(token.email); + } + } + } + } + throw lastError || new Error('Request failed after retries'); + } + + // Handle SSE Stream conversion + private processStreamResponse(upstreamStream: any, model: string): Observable { + return new Observable((subscriber) => { + const decoder = new TextDecoder(); + let buffer = ''; + + upstreamStream.on('data', (chunk: Buffer) => { + buffer += decoder.decode(chunk, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith('data: ')) continue; + + const dataStr = trimmed.slice(6); + if (dataStr === '[DONE]') continue; + + try { + const json = JSON.parse(dataStr); + const candidate = json.candidates?.[0]; + const contentPart = candidate?.content?.parts?.[0]; + const text = contentPart?.text || ''; + + const openAICheck = { + id: `chatcmpl-${uuidv4()}`, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: model, + choices: [ + { + index: 0, + delta: { content: text }, + finish_reason: candidate?.finishReason ? 'stop' : null, + }, + ], + }; + + subscriber.next(`data: ${JSON.stringify(openAICheck)}\n\n`); + + if (candidate?.finishReason) { + subscriber.next('data: [DONE]\n\n'); + subscriber.complete(); + } + } catch (e) { + // ignore parse errors + } + } + }); + + upstreamStream.on('end', () => { + subscriber.complete(); + }); + + upstreamStream.on('error', (err: any) => { + subscriber.error(err); + }); + }); + } + + private convertOpenAIToGemini(request: OpenAIChatRequest): GeminiRequest { + const contents: GeminiContent[] = []; + let systemInstruction: { parts: GeminiPart[] } | undefined = undefined; + + // Extract system message + const messages = request.messages || []; + for (const msg of messages) { + if (msg.role === 'system') { + if (typeof msg.content === 'string') { + systemInstruction = { + parts: [{ text: msg.content }], + }; + } + } else { + let textPart = ''; + if (Array.isArray(msg.content)) { + // OpenAI multimodal array + textPart = msg.content + .filter((p: OpenAIContentPart) => p.type === 'text') + .map((p: OpenAIContentPart) => p.text || '') + .join('\n'); + } else { + textPart = msg.content; + } + + contents.push({ + role: msg.role === 'user' ? 'user' : 'model', + parts: [{ text: textPart }], + }); + } + } + + return { + contents, + systemInstruction, + generationConfig: { + temperature: request.temperature, + maxOutputTokens: request.max_tokens, + topP: request.top_p, + }, + }; + } + + private convertGeminiToOpenAIResponse( + geminiResponse: GeminiResponse, + model: string, + ): OpenAIChatResponse { + const candidate = geminiResponse.candidates?.[0]; + if (!candidate) throw new Error('No candidates in response'); + + // Process response for images + const processedCandidate = this.processInlineData(candidate); + + const parts = processedCandidate?.content?.parts || []; + const content = parts.map((p) => p.text || '').join(''); + + const finishReason = + candidate?.finishReason === 'STOP' + ? 'stop' + : candidate?.finishReason?.toLowerCase() || 'stop'; + + return { + id: `chatcmpl-${uuidv4()}`, + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: model, + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: content, + }, + finish_reason: finishReason, + }, + ], + usage: { + prompt_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, + completion_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0, + total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0, + }, + }; + } + + // Convert Gemini inlineData (images) to Markdown + private processInlineData(candidate: GeminiCandidate | undefined): GeminiCandidate | undefined { + if (!candidate?.content?.parts) return candidate; + + const newParts = candidate.content.parts.map((part) => { + if (part.inlineData) { + const mimeType = part.inlineData.mimeType || 'image/jpeg'; + const data = part.inlineData.data || ''; + return { + text: `\n\n![Generated Image](data:${mimeType};base64,${data})\n\n`, + }; + } + return part; + }); + + return { + ...candidate, + content: { + ...candidate.content, + parts: newParts, + }, + }; + } + + private mapModel(model: string): string { + const lower = model.toLowerCase(); + if (lower.includes('sonnet') || lower.includes('thinking')) return 'gemini-3-pro-preview'; + if (lower.includes('haiku')) return 'gemini-2.0-flash-exp'; + if (lower.includes('opus')) return 'gemini-3-pro-preview'; + if (lower.includes('claude')) return 'gemini-2.5-flash-thinking'; + if (lower === 'gemini-3-pro-high' || lower === 'gemini-3-pro-low') + return 'gemini-3-pro-preview'; + if (lower === 'gemini-3-flash') return 'gemini-3-flash-preview'; + + // Default to original if no mapping found + return model; + } + + private shouldRetry(errorMessage: string): boolean { + const msg = errorMessage.toLowerCase(); + if ( + msg.includes('404') || + msg.includes('not_found') || + msg.includes('403') || + msg.includes('permission_denied') + ) + return true; + if ( + msg.includes('429') || + msg.includes('resource_exhausted') || + msg.includes('quota') || + msg.includes('rate_limit') + ) + return true; + return false; + } +} diff --git a/src/server/modules/proxy/token-manager.service.ts b/src/server/modules/proxy/token-manager.service.ts new file mode 100644 index 0000000..be05966 --- /dev/null +++ b/src/server/modules/proxy/token-manager.service.ts @@ -0,0 +1,202 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { CloudAccountRepo } from '../../../ipc/database/cloudHandler'; +import { CloudAccount } from '../../../types/cloudAccount'; +import { GoogleAPIService } from '../../../services/GoogleAPIService'; + +interface TokenData { + email: string; + account_id: string; + access_token: string; + refresh_token: string; + expires_in: number; + expiry_timestamp: number; + project_id?: string; + session_id?: string; +} + +@Injectable() +export class TokenManagerService implements OnModuleInit { + private readonly logger = new Logger(TokenManagerService.name); + private currentIndex = 0; + // In-memory cache of tokens with additional data + private tokens: Map = new Map(); + // Cooldown map for rate-limited accounts + private cooldowns: Map = new Map(); + + async onModuleInit() { + // Load accounts on module initialization + await this.loadAccounts(); + } + + async loadAccounts(): Promise { + try { + const accounts = await CloudAccountRepo.getAccounts(); + let count = 0; + + for (const account of accounts) { + const tokenData = this.convertAccountToToken(account); + if (tokenData) { + this.tokens.set(account.id, tokenData); + count++; + } + } + + this.logger.log(`Loaded ${count} accounts`); + return count; + } catch (e) { + this.logger.error('Failed to load accounts', e); + return 0; + } + } + + private convertAccountToToken(account: CloudAccount): TokenData | null { + if (!account.token) return null; + + return { + account_id: account.id, + email: account.email, + access_token: account.token.access_token, + refresh_token: account.token.refresh_token, + expires_in: account.token.expires_in, + expiry_timestamp: account.token.expiry_timestamp, + project_id: account.token.project_id || undefined, + session_id: account.token.session_id || this.generateSessionId(), + }; + } + + private generateSessionId(): string { + // Match the Rust implementation's session ID format + const min = 1_000_000_000_000_000_000n; + const max = 9_000_000_000_000_000_000n; + const range = max - min; + const rand = BigInt(Math.floor(Math.random() * Number(range))); + return (-(min + rand)).toString(); + } + + async getNextToken(): Promise { + try { + // Reload if empty + if (this.tokens.size === 0) { + await this.loadAccounts(); + } + if (this.tokens.size === 0) return null; + + const now = Date.now(); + const nowSeconds = Math.floor(now / 1000); + + // Filter out accounts in cooldown + const validTokens = Array.from(this.tokens.entries()).filter(([email, _]) => { + const cooldownUntil = this.cooldowns.get(email); + return !cooldownUntil || cooldownUntil <= now; + }); + + if (validTokens.length === 0) { + this.logger.warn('All accounts are in cooldown'); + return null; + } + + // Round robin selection + const [accountId, tokenData] = validTokens[this.currentIndex % validTokens.length]; + this.currentIndex++; + + // Check if token needs refresh (expires in < 5 minutes) + if (nowSeconds >= tokenData.expiry_timestamp - 300) { + this.logger.log(`Token for ${tokenData.email} expiring soon, refreshing...`); + try { + const newTokens = await GoogleAPIService.refreshAccessToken(tokenData.refresh_token); + + // Update token data + tokenData.access_token = newTokens.access_token; + tokenData.expires_in = newTokens.expires_in; + tokenData.expiry_timestamp = nowSeconds + newTokens.expires_in; + + // Save to DB + await this.saveRefreshedToken(accountId, tokenData); + this.tokens.set(accountId, tokenData); + + this.logger.log(`Token refreshed for ${tokenData.email}`); + } catch (e) { + this.logger.error(`Failed to refresh token for ${tokenData.email}`, e); + } + } + + // Resolve project ID if missing (mock for now, like original) + if (!tokenData.project_id) { + const mockId = `cloud-code-${Math.floor(Math.random() * 100000)}`; + tokenData.project_id = mockId; + await this.saveProjectId(accountId, mockId); + } + + this.logger.log(`Selected account: ${tokenData.email}`); + + // Return in CloudAccount format for compatibility + return { + id: accountId, + email: tokenData.email, + token: { + access_token: tokenData.access_token, + refresh_token: tokenData.refresh_token, + expires_in: tokenData.expires_in, + expiry_timestamp: tokenData.expiry_timestamp, + project_id: tokenData.project_id, + session_id: tokenData.session_id, + }, + } as CloudAccount; + } catch (error) { + this.logger.error('Failed to get token', error); + return null; + } + } + + markAsRateLimited(email: string) { + // Cooldown for 5 minutes + const until = Date.now() + 5 * 60 * 1000; + this.cooldowns.set(email, until); + this.logger.warn( + `Account ${email} marked as rate limited until ${new Date(until).toISOString()}`, + ); + } + + resetCooldown(email: string) { + this.cooldowns.delete(email); + } + + private async saveRefreshedToken(accountId: string, tokenData: TokenData) { + try { + const acc = await CloudAccountRepo.getAccount(accountId); + if (acc && acc.token) { + const newToken = { + ...acc.token, + access_token: tokenData.access_token, + expires_in: tokenData.expires_in, + expiry_timestamp: tokenData.expiry_timestamp, + }; + await CloudAccountRepo.updateToken(accountId, newToken); + } + } catch (e) { + this.logger.error('Failed to save refreshed token to DB', e); + } + } + + private async saveProjectId(accountId: string, projectId: string) { + try { + const acc = await CloudAccountRepo.getAccount(accountId); + if (acc && acc.token) { + const newToken = { + ...acc.token, + project_id: projectId, + }; + await CloudAccountRepo.updateToken(accountId, newToken); + } + } catch (e) { + this.logger.error('Failed to save project ID to DB', e); + } + } + + /** + * Get the number of loaded accounts (for status) + */ + getAccountCount(): number { + return this.tokens.size; + } +}