diff --git a/ecotoken-extension/.gitignore b/ecotoken-extension/.gitignore index 7fa9b91..4d993d5 100644 --- a/ecotoken-extension/.gitignore +++ b/ecotoken-extension/.gitignore @@ -49,6 +49,7 @@ server/*.pem server/*.key server/*.p8 server/*.pub +server/.test-rsa-key.p8 # Coverage coverage/ diff --git a/ecotoken-extension/package-lock.json b/ecotoken-extension/package-lock.json index e67b3e3..9157ec3 100644 --- a/ecotoken-extension/package-lock.json +++ b/ecotoken-extension/package-lock.json @@ -27,7 +27,8 @@ "globals": "^16.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", - "vite": "npm:rolldown-vite@7.1.14" + "vite": "npm:rolldown-vite@7.1.14", + "vitest": "^3.2.4" } }, "node_modules/@babel/code-frame": { @@ -956,6 +957,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/chrome": { "version": "0.1.40", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.40.tgz", @@ -967,6 +979,13 @@ "@types/har-format": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1327,6 +1346,121 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1412,6 +1546,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1504,6 +1648,16 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1535,6 +1689,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1552,6 +1723,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1662,6 +1843,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1706,6 +1897,13 @@ "dev": true, "license": "ISC" }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1897,6 +2095,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1907,6 +2115,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2676,6 +2894,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2686,6 +2911,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2865,6 +3100,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3138,6 +3390,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3147,6 +3406,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3160,6 +3433,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3173,6 +3466,20 @@ "node": ">=8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3218,6 +3525,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3434,6 +3771,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-static-copy": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.3.tgz", @@ -3482,6 +3842,92 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3498,6 +3944,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/ecotoken-extension/package.json b/ecotoken-extension/package.json index 84bd80b..a9103d7 100644 --- a/ecotoken-extension/package.json +++ b/ecotoken-extension/package.json @@ -8,7 +8,9 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "dev:api": "node server/server.mjs" + "dev:api": "node server/server.mjs", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "dotenv": "^17.2.3", @@ -30,7 +32,8 @@ "globals": "^16.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", - "vite": "npm:rolldown-vite@7.1.14" + "vite": "npm:rolldown-vite@7.1.14", + "vitest": "^3.2.4" }, "overrides": { "vite": "npm:rolldown-vite@7.1.14" diff --git a/ecotoken-extension/server/api.test.mjs b/ecotoken-extension/server/api.test.mjs new file mode 100644 index 0000000..10a285d --- /dev/null +++ b/ecotoken-extension/server/api.test.mjs @@ -0,0 +1,142 @@ +import { generateKeyPairSync } from "node:crypto"; +import { unlinkSync, writeFileSync } from "node:fs"; +import http from "node:http"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const testKeyFile = join(__dirname, ".test-rsa-key.p8"); + +function request(port, opts) { + const { method = "GET", path = "/", body, headers = {} } = opts; + return new Promise((resolve, reject) => { + const req = http.request( + { hostname: "127.0.0.1", port, path, method, headers }, + (res) => { + let data = ""; + res.on("data", (c) => { + data += c; + }); + res.on("end", () => { + resolve({ status: res.statusCode, headers: res.headers, body: data }); + }); + } + ); + req.on("error", reject); + if (body != null) req.write(typeof body === "string" ? body : JSON.stringify(body)); + req.end(); + }); +} + +/** Mock Snowflake SQL API: OPTIMIZE returns a variant JSON string in `data[0][0]`. */ +async function mockSnowflakeFetch(url, init) { + const u = String(url); + if (!u.includes("snowflakecomputing.com/api/v2/statements")) { + return new Response("unexpected URL", { status: 500 }); + } + const payload = JSON.parse(init.body); + const stmt = payload.statement || ""; + + if (stmt.includes("OPTIMIZE")) { + const variant = { + optimized_text: "mocked-short", + estimated_tokens_before: 8, + estimated_tokens_after: 4, + estimated_tokens_saved: 4, + model: "mock" + }; + return new Response(JSON.stringify({ data: [[JSON.stringify(variant)]] }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + } + + return new Response(JSON.stringify({}), { + status: 200, + headers: { "Content-Type": "application/json" } + }); +} + +describe("POST /api/optimize", () => { + let server; + let port; + let createApiServer; + + beforeAll(async () => { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + writeFileSync(testKeyFile, privateKey.export({ type: "pkcs8", format: "pem" })); + + process.env.NODE_ENV = "test"; + process.env.SNOWFLAKE_HTTP_ACCOUNT = "test-account"; + process.env.SNOWFLAKE_ACCOUNT = "TESTACCT"; + process.env.SNOWFLAKE_USER = "TESTUSER"; + process.env.SNOWFLAKE_PUBLIC_KEY_FP = "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + process.env.SNOWFLAKE_WAREHOUSE = "WH"; + process.env.SNOWFLAKE_DATABASE = "DB"; + process.env.SNOWFLAKE_SCHEMA = "SC"; + process.env.SNOWFLAKE_LOG_SCHEMA = "SC"; + process.env.SNOWFLAKE_ROLE = "RL"; + process.env.SF_PRIVATE_KEY_PATH = "./.test-rsa-key.p8"; + + vi.stubGlobal("fetch", vi.fn(mockSnowflakeFetch)); + + ({ createApiServer } = await import("./http-app.mjs")); + server = createApiServer(); + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", resolve); + }); + port = server.address().port; + }); + + afterAll(async () => { + await new Promise((resolve) => server.close(resolve)); + try { + unlinkSync(testKeyFile); + } catch { + /* ignore */ + } + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it("returns 400 when tag is missing", async () => { + const { status, body } = await request(port, { + method: "POST", + path: "/api/optimize", + headers: { "Content-Type": "application/json" }, + body: { raw_text: "hello" } + }); + expect(status).toBe(400); + expect(JSON.parse(body)).toEqual({ error: "MISSING_TAG" }); + }); + + it("returns 200 with optimized payload when Snowflake succeeds", async () => { + const { status, body } = await request(port, { + method: "POST", + path: "/api/optimize", + headers: { "Content-Type": "application/json", Origin: "https://chatgpt.com" }, + body: { tag: "demo", raw_text: "please shorten this prompt", model: null } + }); + expect(status).toBe(200); + const json = JSON.parse(body); + expect(json.optimized_text).toBe("mocked-short"); + expect(json.estimated_tokens_saved).toBe(4); + expect(fetch).toHaveBeenCalled(); + }); + + it("handles OPTIONS /api/optimize for CORS preflight", async () => { + const { status } = await request(port, { + method: "OPTIONS", + path: "/api/optimize", + headers: { Origin: "https://chatgpt.com" } + }); + expect(status).toBe(200); + }); + + it("returns 404 for unknown routes", async () => { + const { status, body } = await request(port, { method: "GET", path: "/" }); + expect(status).toBe(404); + expect(body).toBe("Not found"); + }); +}); diff --git a/ecotoken-extension/server/http-app.mjs b/ecotoken-extension/server/http-app.mjs new file mode 100644 index 0000000..b93dc96 --- /dev/null +++ b/ecotoken-extension/server/http-app.mjs @@ -0,0 +1,229 @@ +import fs from "fs"; +import http from "http"; +import jwt from "jsonwebtoken"; + +const { + SNOWFLAKE_HTTP_ACCOUNT, + SNOWFLAKE_ACCOUNT, + SNOWFLAKE_USER, + SNOWFLAKE_PUBLIC_KEY_FP, + SNOWFLAKE_WAREHOUSE, + SNOWFLAKE_DATABASE, + SNOWFLAKE_SCHEMA, + SNOWFLAKE_LOG_SCHEMA = "ECOTOKEN", + SNOWFLAKE_ROLE, + SF_PRIVATE_KEY_PATH = "./rsa_key.p8", + SF_PRIVATE_KEY_PASSPHRASE, + EXTENSION_ID, + ALLOW_ORIGIN, +} = process.env; + +function makeSnowflakeJWT() { + const account = SNOWFLAKE_ACCOUNT.toUpperCase(); + const user = SNOWFLAKE_USER.toUpperCase(); + const iss = `${account}.${user}.${SNOWFLAKE_PUBLIC_KEY_FP}`; + const sub = `${account}.${user}`; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 59 * 60; + + const key = SF_PRIVATE_KEY_PASSPHRASE + ? { + key: fs.readFileSync(new URL(SF_PRIVATE_KEY_PATH, import.meta.url)), + passphrase: SF_PRIVATE_KEY_PASSPHRASE, + } + : fs.readFileSync(new URL(SF_PRIVATE_KEY_PATH, import.meta.url)); + + return jwt.sign({ iss, sub, iat, exp }, key, { algorithm: "RS256" }); +} + +async function execSqlApi(statement, bindings) { + const token = makeSnowflakeJWT(); + const url = `https://${SNOWFLAKE_HTTP_ACCOUNT}.snowflakecomputing.com/api/v2/statements`; + const payload = { + statement, + warehouse: SNOWFLAKE_WAREHOUSE, + database: SNOWFLAKE_DATABASE, + schema: SNOWFLAKE_SCHEMA, + role: SNOWFLAKE_ROLE, + timeout: 60, + bindings, + }; + const r = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT", + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(payload), + }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +async function insertPromptSample(tag, rawText) { + if (!rawText) return false; + const tableFqn = `${SNOWFLAKE_DATABASE}.${SNOWFLAKE_LOG_SCHEMA}.PROMPT_TUNING`; + for (const tsCol of ["CREATED_AT", "created_ts"]) { + try { + const stmt = `INSERT INTO ${tableFqn} (tag, raw_sample, ${tsCol}) VALUES (?, ?, CURRENT_TIMESTAMP())`; + await execSqlApi(stmt, { + "1": { type: "TEXT", value: tag }, + "2": { type: "TEXT", value: rawText }, + }); + console.log(`[EcoToken API] inserted prompt`, { table: tableFqn, tsCol, tag, textLen: rawText?.length ?? 0 }); + return true; + } catch (e) { + console.error(`[EcoToken API] insert failed`, { table: tableFqn, tsCol, error: String(e) }); + } + } + return false; +} + +async function optimizeViaSqlApi(tag, raw_filter = null, model = null) { + const token = makeSnowflakeJWT(); + const procFqn = `${SNOWFLAKE_DATABASE}.${SNOWFLAKE_SCHEMA}.OPTIMIZE`; + const payload = { + statement: `CALL ${procFqn}(?, ?, ?)`, + warehouse: SNOWFLAKE_WAREHOUSE, + database: SNOWFLAKE_DATABASE, + schema: SNOWFLAKE_SCHEMA, + role: SNOWFLAKE_ROLE, + timeout: 60, + bindings: { + "1": { type: "TEXT", value: tag }, + "2": { type: "TEXT", value: raw_filter }, + "3": { type: "TEXT", value: model }, + }, + }; + + const url = `https://${SNOWFLAKE_HTTP_ACCOUNT}.snowflakecomputing.com/api/v2/statements`; + const r = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT", + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(payload), + }); + if (!r.ok) throw new Error(await r.text()); + const data = await r.json(); + console.log(`[EcoToken API] optimize call done`, { tag, hasRowset: Array.isArray(data?.rowset), hasData: Array.isArray(data?.data) }); + + const tryParse = (v) => { + if (v == null) return v; + if (typeof v === "string") { + try { + return JSON.parse(v); + } catch { + return v; + } + } + return v; + }; + + if (Array.isArray(data?.rowset) && data.rowset.length > 0) { + const row = data.rowset[0]; + const cols = data?.resultSetMetaData?.rowType?.map((c) => c.name?.toLowerCase?.()) || []; + if (row.length === 1) return tryParse(row[0]); + const obj = {}; + for (let i = 0; i < row.length; i++) obj[cols[i] || String(i)] = tryParse(row[i]); + return obj; + } + if (Array.isArray(data?.data) && data.data.length > 0) { + const row = data.data[0]; + return Array.isArray(row) ? tryParse(row[0]) : row; + } + return null; +} + +async function insertOptimizationStats(tag, stats) { + try { + if (!stats) return false; + const before = stats.estimated_tokens_before ?? null; + const after = stats.estimated_tokens_after ?? null; + const saved = stats.estimated_tokens_saved ?? (before != null && after != null ? before - after : null); + const model = stats.model ?? null; + if (before == null && after == null && saved == null && !model) return false; + + const procFqn = `${SNOWFLAKE_DATABASE}.${SNOWFLAKE_SCHEMA}.LOG_STATS_TEST`; + const stmt = `CALL ${procFqn}(?, ?, ?, ?, ?)`; + await execSqlApi(stmt, { + "1": { type: "TEXT", value: tag }, + "2": { type: "TEXT", value: model }, + "3": { type: "TEXT", value: before == null ? null : String(before) }, + "4": { type: "TEXT", value: after == null ? null : String(after) }, + "5": { type: "TEXT", value: saved == null ? null : String(saved) }, + }); + console.log(`[EcoToken API] logged stats via proc`, { proc: procFqn, tag, model, before, after, saved }); + return true; + } catch (e) { + console.warn(`[EcoToken API] stats proc failed`, { error: String(e) }); + return false; + } +} + +function corsHeaders(origin) { + const allowed = new Set([EXTENSION_ID ? `chrome-extension://${EXTENSION_ID}` : null, ALLOW_ORIGIN || null, "http://localhost:3001"].filter(Boolean)); + const ok = origin && (allowed.has(origin) || process.env.NODE_ENV !== "production"); + return { + "Access-Control-Allow-Origin": ok ? origin : "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + Vary: "Origin", + }; +} + +export function createApiServer() { + return http.createServer(async (req, res) => { + const origin = req.headers.origin; + const cors = corsHeaders(origin); + + if (req.method === "OPTIONS" && req.url === "/api/optimize") { + res.writeHead(200, cors); + res.end(); + return; + } + + if (req.method === "POST" && req.url === "/api/optimize") { + try { + const body = await new Promise((resolve) => { + const bufs = []; + req.on("data", (c) => bufs.push(c)); + req.on("end", () => resolve(Buffer.concat(bufs).toString("utf8"))); + }); + const { tag, raw_filter = null, model = null, raw_text = null } = body ? JSON.parse(body) : {}; + if (!tag) { + res.writeHead(400, { ...cors, "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "MISSING_TAG" })); + return; + } + + console.log(`[EcoToken API] /optimize`, { tag, rawTextLen: raw_text?.length ?? 0 }); + try { + await insertPromptSample(tag, raw_text); + } catch {} + + const out = await optimizeViaSqlApi(tag, raw_text ?? raw_filter, model); + if (out && typeof out === "object") { + await insertOptimizationStats(tag, out); + } + if (!out) console.warn(`[EcoToken API] optimize returned empty`, { tag }); + res.writeHead(200, { ...cors, "Content-Type": "application/json" }); + res.end(JSON.stringify(out || { error: "NO_RESULT" })); + return; + } catch (e) { + console.error(`[EcoToken API] handler error`, e); + res.writeHead(500, { ...cors, "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "BACKEND_EXCEPTION", detail: String(e) })); + return; + } + } + + res.writeHead(404, cors); + res.end("Not found"); + }); +} diff --git a/ecotoken-extension/server/server.mjs b/ecotoken-extension/server/server.mjs index 8fa3866..455c993 100644 --- a/ecotoken-extension/server/server.mjs +++ b/ecotoken-extension/server/server.mjs @@ -1,226 +1,8 @@ -import fs from "fs"; -import http from "http"; -import jwt from "jsonwebtoken"; import dotenv from "dotenv"; -dotenv.config({ path: new URL("./.env", import.meta.url) }); // load server/.env +dotenv.config({ path: new URL("./.env", import.meta.url) }); -const { - PORT = "3000", - SNOWFLAKE_HTTP_ACCOUNT, - SNOWFLAKE_ACCOUNT, - SNOWFLAKE_USER, - SNOWFLAKE_PUBLIC_KEY_FP, - SNOWFLAKE_WAREHOUSE, - SNOWFLAKE_DATABASE, - SNOWFLAKE_SCHEMA, - SNOWFLAKE_LOG_SCHEMA = "ECOTOKEN", - SNOWFLAKE_ROLE, - SF_PRIVATE_KEY_PATH = "./rsa_key.p8", - SF_PRIVATE_KEY_PASSPHRASE, - EXTENSION_ID, - ALLOW_ORIGIN -} = process.env; - -function makeSnowflakeJWT() { - const account = SNOWFLAKE_ACCOUNT.toUpperCase(); - const user = SNOWFLAKE_USER.toUpperCase(); - const iss = `${account}.${user}.${SNOWFLAKE_PUBLIC_KEY_FP}`; - const sub = `${account}.${user}`; - const iat = Math.floor(Date.now()/1000); - const exp = iat + 59*60; - - const key = SF_PRIVATE_KEY_PASSPHRASE - ? { key: fs.readFileSync(new URL(SF_PRIVATE_KEY_PATH, import.meta.url)), passphrase: SF_PRIVATE_KEY_PASSPHRASE } - : fs.readFileSync(new URL(SF_PRIVATE_KEY_PATH, import.meta.url)); - - return jwt.sign({ iss, sub, iat, exp }, key, { algorithm: "RS256" }); -} - -async function execSqlApi(statement, bindings) { - const token = makeSnowflakeJWT(); - const url = `https://${SNOWFLAKE_HTTP_ACCOUNT}.snowflakecomputing.com/api/v2/statements`; - const payload = { - statement, - warehouse: SNOWFLAKE_WAREHOUSE, - database: SNOWFLAKE_DATABASE, - schema: SNOWFLAKE_SCHEMA, - role: SNOWFLAKE_ROLE, - timeout: 60, - bindings - }; - const r = await fetch(url, { - method: "POST", - headers: { - "Authorization": `Bearer ${token}`, - "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT", - "Content-Type": "application/json", - "Accept": "application/json" - }, - body: JSON.stringify(payload) - }); - if (!r.ok) throw new Error(await r.text()); - return r.json(); -} - -async function insertPromptSample(tag, rawText) { - if (!rawText) return false; // nothing to write - // Insert into ECOTOKEN_DB.ECOTOKEN.PROMPT_TUNING by default (matches proc) - const tableFqn = `${SNOWFLAKE_DATABASE}.${SNOWFLAKE_LOG_SCHEMA}.PROMPT_TUNING`; - for (const tsCol of ["CREATED_AT", "created_ts"]) { - try { - const stmt = `INSERT INTO ${tableFqn} (tag, raw_sample, ${tsCol}) VALUES (?, ?, CURRENT_TIMESTAMP())`; - await execSqlApi(stmt, { - "1": { type: "TEXT", value: tag }, - "2": { type: "TEXT", value: rawText } - }); - console.log(`[EcoToken API] inserted prompt`, { table: tableFqn, tsCol, tag, textLen: rawText?.length ?? 0 }); - return true; // success - } catch (e) { - console.error(`[EcoToken API] insert failed`, { table: tableFqn, tsCol, error: String(e) }); - } - } - // swallow insert errors so optimization can still proceed - proc can use raw text fallback - return false; -} - -async function optimizeViaSqlApi(tag, raw_filter = null, model = null) { - const token = makeSnowflakeJWT(); - const procFqn = `${SNOWFLAKE_DATABASE}.${SNOWFLAKE_SCHEMA}.OPTIMIZE`; - const payload = { - statement: `CALL ${procFqn}(?, ?, ?)`, - warehouse: SNOWFLAKE_WAREHOUSE, - database: SNOWFLAKE_DATABASE, - schema: SNOWFLAKE_SCHEMA, - role: SNOWFLAKE_ROLE, - timeout: 60, - bindings: { - "1": { type: "TEXT", value: tag }, - "2": { type: "TEXT", value: raw_filter }, - "3": { type: "TEXT", value: model } - } - }; - - const url = `https://${SNOWFLAKE_HTTP_ACCOUNT}.snowflakecomputing.com/api/v2/statements`; - const r = await fetch(url, { - method: "POST", - headers: { - "Authorization": `Bearer ${token}`, - "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT", - "Content-Type": "application/json", - "Accept": "application/json" - }, - body: JSON.stringify(payload) - }); - if (!r.ok) throw new Error(await r.text()); - const data = await r.json(); - console.log(`[EcoToken API] optimize call done`, { tag, hasRowset: Array.isArray(data?.rowset), hasData: Array.isArray(data?.data) }); - - // Robustly unwrap SQL API response for CALL results - const tryParse = (v) => { - if (v == null) return v; - if (typeof v === "string") { - try { return JSON.parse(v); } catch { return v; } - } - return v; - }; - - if (Array.isArray(data?.rowset) && data.rowset.length > 0) { - const row = data.rowset[0]; - const cols = data?.resultSetMetaData?.rowType?.map(c => c.name?.toLowerCase?.()) || []; - if (row.length === 1) return tryParse(row[0]); - const obj = {}; - for (let i = 0; i < row.length; i++) obj[cols[i] || String(i)] = tryParse(row[i]); - return obj; - } - if (Array.isArray(data?.data) && data.data.length > 0) { - const row = data.data[0]; - return Array.isArray(row) ? tryParse(row[0]) : row; - } - return null; -} - -async function insertOptimizationStats(tag, stats) { - try { - if (!stats) return false; - const before = stats.estimated_tokens_before ?? null; - const after = stats.estimated_tokens_after ?? null; - const saved = stats.estimated_tokens_saved ?? ((before != null && after != null) ? (before - after) : null); - const model = stats.model ?? null; - if (before == null && after == null && saved == null && !model) return false; - - const procFqn = `${SNOWFLAKE_DATABASE}.${SNOWFLAKE_SCHEMA}.LOG_STATS_TEST`; - const stmt = `CALL ${procFqn}(?, ?, ?, ?, ?)`; - await execSqlApi(stmt, { - "1": { type: "TEXT", value: tag }, - "2": { type: "TEXT", value: model }, - // Send numerics as TEXT to avoid driver integer type issues; proc will coerce - "3": { type: "TEXT", value: before == null ? null : String(before) }, - "4": { type: "TEXT", value: after == null ? null : String(after) }, - "5": { type: "TEXT", value: saved == null ? null : String(saved) } - }); - console.log(`[EcoToken API] logged stats via proc`, { proc: procFqn, tag, model, before, after, saved }); - return true; - } catch (e) { - console.warn(`[EcoToken API] stats proc failed`, { error: String(e) }); - return false; - } -} - -function corsHeaders(origin) { - const allowed = new Set([ - EXTENSION_ID ? `chrome-extension://${EXTENSION_ID}` : null, - ALLOW_ORIGIN || null, - "http://localhost:3001", // dev tools - ].filter(Boolean)); - const ok = origin && (allowed.has(origin) || process.env.NODE_ENV !== "production"); - return { - "Access-Control-Allow-Origin": ok ? origin : "*", - "Access-Control-Allow-Methods": "POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - "Vary": "Origin" - }; -} - -const server = http.createServer(async (req, res) => { - const origin = req.headers.origin; - const cors = corsHeaders(origin); - - if (req.method === "OPTIONS" && req.url === "/api/optimize") { - res.writeHead(200, cors); res.end(); return; - } - - if (req.method === "POST" && req.url === "/api/optimize") { - try { - const body = await new Promise((resolve) => { - const bufs = []; req.on("data", c => bufs.push(c)); - req.on("end", () => resolve(Buffer.concat(bufs).toString("utf8"))); - }); - const { tag, raw_filter = null, model = null, raw_text = null } = body ? JSON.parse(body) : {}; - if (!tag) { res.writeHead(400, { ...cors, "Content-Type": "application/json" }); res.end(JSON.stringify({ error:"MISSING_TAG" })); return; } - - // 1) Persist the user's current prompt (optional) - console.log(`[EcoToken API] /optimize`, { tag, rawTextLen: raw_text?.length ?? 0 }); - try { await insertPromptSample(tag, raw_text); } catch {} - - // 2) Optimize; also pass raw_text so the proc can fallback to it if no row - const out = await optimizeViaSqlApi(tag, raw_text ?? raw_filter, model); - // 3) Log analytics (best-effort) - if (out && typeof out === "object") { - await insertOptimizationStats(tag, out); - } - if (!out) console.warn(`[EcoToken API] optimize returned empty`, { tag }); - res.writeHead(200, { ...cors, "Content-Type": "application/json" }); - res.end(JSON.stringify(out || { error: "NO_RESULT" })); return; - } catch (e) { - console.error(`[EcoToken API] handler error`, e); - res.writeHead(500, { ...cors, "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "BACKEND_EXCEPTION", detail: String(e) })); return; - } - } - - res.writeHead(404, cors); res.end("Not found"); -}); - -server.listen(Number(PORT), () => { +const { createApiServer } = await import("./http-app.mjs"); +const { PORT = "3000" } = process.env; +createApiServer().listen(Number(PORT), () => { console.log(`[EcoToken API] http://localhost:${PORT}`); }); diff --git a/ecotoken-extension/tsconfig.node.json b/ecotoken-extension/tsconfig.node.json index 8a67f62..7ad54d4 100644 --- a/ecotoken-extension/tsconfig.node.json +++ b/ecotoken-extension/tsconfig.node.json @@ -22,5 +22,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vitest.config.ts"] } diff --git a/ecotoken-extension/vitest.config.ts b/ecotoken-extension/vitest.config.ts new file mode 100644 index 0000000..8259833 --- /dev/null +++ b/ecotoken-extension/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["server/**/*.test.mjs"], + environment: "node", + }, +});