From 349fe7debecbd67c9e9bb5f6c69d8e0032cbd921 Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:43:46 +0800 Subject: [PATCH 01/18] chore: remediate audit-ci dependency findings --- package-lock.json | 7534 ++++++++++++++++++++--------------------- package.json | 9 +- test/accounts.test.ts | 4 +- 3 files changed, 3776 insertions(+), 3771 deletions(-) diff --git a/package-lock.json b/package-lock.json index e305f73f0..de0cb0a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3769 +1,3769 @@ { - "name": "codex-multi-auth", - "version": "1.2.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "codex-multi-auth", - "version": "1.2.2", - "bundleDependencies": [ - "@codex-ai/plugin" - ], - "license": "MIT", - "dependencies": { - "@codex-ai/plugin": "file:vendor/codex-ai-plugin", - "@openauthjs/openauth": "^0.4.3", - "hono": "4.12.6", - "undici": "^6.24.1", - "zod": "^4.3.6" - }, - "bin": { - "codex": "scripts/codex.js", - "codex-multi-auth": "scripts/codex-multi-auth.js" - }, - "devDependencies": { - "@codex-ai/sdk": "file:vendor/codex-ai-sdk", - "@fast-check/vitest": "^0.2.4", - "@types/node": "^25.3.0", - "@typescript-eslint/eslint-plugin": "^8.56.0", - "@typescript-eslint/parser": "^8.56.0", - "@vitest/coverage-v8": "^4.0.18", - "@vitest/ui": "^4.0.18", - "eslint": "^10.0.0", - "fast-check": "^4.5.3", - "husky": "^9.1.7", - "lint-staged": "^16.2.7", - "typescript": "^5.9.3", - "typescript-language-server": "^5.1.3", - "vitest": "^4.0.18" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.6" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@codex-ai/plugin": { - "resolved": "vendor/codex-ai-plugin", - "link": true - }, - "node_modules/@codex-ai/sdk": { - "resolved": "vendor/codex-ai-sdk", - "link": true - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", - "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.1", - "debug": "^4.3.1", - "minimatch": "^10.1.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", - "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@fast-check/vitest": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@fast-check/vitest/-/vitest-0.2.4.tgz", - "integrity": "sha512-Ilcr+JAIPhb1s6FRm4qoglQYSGXXrS+zAupZeNuWAA3qHVGDA1d1Gb84Hb/+otL3GzVZjFJESg5/1SfIvrgssA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "fast-check": "^3.0.0 || ^4.0.0" - }, - "peerDependencies": { - "vitest": "^1 || ^2 || ^3 || ^4" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@openauthjs/openauth": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@openauthjs/openauth/-/openauth-0.4.3.tgz", - "integrity": "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw==", - "dependencies": { - "@standard-schema/spec": "1.0.0-beta.3", - "aws4fetch": "1.0.20", - "jose": "5.9.6" - }, - "peerDependencies": { - "arctic": "^2.2.2", - "hono": "^4.0.0" - } - }, - "node_modules/@oslojs/asn1": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz", - "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@oslojs/binary": "1.0.0" - } - }, - "node_modules/@oslojs/binary": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz", - "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@oslojs/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@oslojs/asn1": "1.0.0", - "@oslojs/binary": "1.0.0" - } - }, - "node_modules/@oslojs/encoding": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", - "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@oslojs/jwt": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@oslojs/jwt/-/jwt-0.2.0.tgz", - "integrity": "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@oslojs/encoding": "0.4.1" - } - }, - "node_modules/@oslojs/jwt/node_modules/@oslojs/encoding": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-0.4.1.tgz", - "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", - "license": "MIT", - "peer": true - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0-beta.3.tgz", - "integrity": "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw==", - "license": "MIT" - }, - "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/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/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", - "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.18", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", - "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.18", - "fflate": "^0.8.2", - "flatted": "^3.3.3", - "pathe": "^2.0.3", - "sirv": "^3.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.0.18" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/arctic": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/arctic/-/arctic-2.3.4.tgz", - "integrity": "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@oslojs/crypto": "1.0.1", - "@oslojs/encoding": "1.1.0", - "@oslojs/jwt": "0.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/ast-v8-to-istanbul": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", - "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" - } - }, - "node_modules/aws4fetch": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.20.tgz", - "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "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" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", - "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.0", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.0", - "eslint-visitor-keys": "^5.0.0", - "espree": "^11.1.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.1.1", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", - "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", - "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "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", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "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-check": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", - "integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^7.0.0" - }, - "engines": { - "node": ">=12.17.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", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hono": { - "version": "4.12.6", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.6.tgz", - "integrity": "sha512-KljEp+MeEEEIOT75qBo1UjqqB29fRMtlDEwCxcexOzdkUq6LR/vRvHk5pdROcxyOYyW1niq7Gb5pFVGy5R1eBw==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "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/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lint-staged": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", - "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^14.0.2", - "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", - "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "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/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "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/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "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/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "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": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "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", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-language-server": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.1.3.tgz", - "integrity": "sha512-r+pAcYtWdN8tKlYZPwiiHNA2QPjXnI02NrW5Sf2cVM3TRtuQ3V9EKKwOxqwaQ0krsaEXk/CbN90I5erBuf84Vg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "typescript-language-server": "lib/cli.mjs" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "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", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "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", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "vendor/codex-ai-plugin": { - "name": "@codex-ai/plugin", - "version": "1.2.10-codex.1" - }, - "vendor/codex-ai-sdk": { - "name": "@codex-ai/sdk", - "version": "1.2.10-codex.1", - "dev": true - } - } + "name": "codex-multi-auth", + "version": "1.2.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-multi-auth", + "version": "1.2.2", + "bundleDependencies": [ + "@codex-ai/plugin" + ], + "license": "MIT", + "dependencies": { + "@codex-ai/plugin": "file:vendor/codex-ai-plugin", + "@openauthjs/openauth": "^0.4.3", + "hono": "4.12.10", + "undici": "^6.24.1", + "zod": "^4.3.6" + }, + "bin": { + "codex": "scripts/codex.js", + "codex-multi-auth": "scripts/codex-multi-auth.js" + }, + "devDependencies": { + "@codex-ai/sdk": "file:vendor/codex-ai-sdk", + "@fast-check/vitest": "^0.2.4", + "@types/node": "^25.3.0", + "@typescript-eslint/eslint-plugin": "^8.56.0", + "@typescript-eslint/parser": "^8.56.0", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", + "eslint": "^10.0.0", + "fast-check": "^4.5.3", + "husky": "^9.1.7", + "lint-staged": "^16.2.7", + "typescript": "^5.9.3", + "typescript-language-server": "^5.1.3", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@codex-ai/plugin": { + "resolved": "vendor/codex-ai-plugin", + "link": true + }, + "node_modules/@codex-ai/sdk": { + "resolved": "vendor/codex-ai-sdk", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", + "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.1", + "debug": "^4.3.1", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", + "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@fast-check/vitest": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@fast-check/vitest/-/vitest-0.2.4.tgz", + "integrity": "sha512-Ilcr+JAIPhb1s6FRm4qoglQYSGXXrS+zAupZeNuWAA3qHVGDA1d1Gb84Hb/+otL3GzVZjFJESg5/1SfIvrgssA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "fast-check": "^3.0.0 || ^4.0.0" + }, + "peerDependencies": { + "vitest": "^1 || ^2 || ^3 || ^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@openauthjs/openauth": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@openauthjs/openauth/-/openauth-0.4.3.tgz", + "integrity": "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw==", + "dependencies": { + "@standard-schema/spec": "1.0.0-beta.3", + "aws4fetch": "1.0.20", + "jose": "5.9.6" + }, + "peerDependencies": { + "arctic": "^2.2.2", + "hono": "^4.0.0" + } + }, + "node_modules/@oslojs/asn1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz", + "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@oslojs/binary": "1.0.0" + } + }, + "node_modules/@oslojs/binary": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz", + "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@oslojs/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@oslojs/asn1": "1.0.0", + "@oslojs/binary": "1.0.0" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@oslojs/jwt": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@oslojs/jwt/-/jwt-0.2.0.tgz", + "integrity": "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@oslojs/encoding": "0.4.1" + } + }, + "node_modules/@oslojs/jwt/node_modules/@oslojs/encoding": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-0.4.1.tgz", + "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0-beta.3.tgz", + "integrity": "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw==", + "license": "MIT" + }, + "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/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/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/arctic": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/arctic/-/arctic-2.3.4.tgz", + "integrity": "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@oslojs/crypto": "1.0.1", + "@oslojs/encoding": "1.1.0", + "@oslojs/jwt": "0.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/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/aws4fetch": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.20.tgz", + "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", + "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.0", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.0", + "eslint-visitor-keys": "^5.0.0", + "espree": "^11.1.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.1.1", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", + "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", + "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "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", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "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-check": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", + "integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^7.0.0" + }, + "engines": { + "node": ">=12.17.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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hono": { + "version": "4.12.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz", + "integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "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/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/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/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "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", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-language-server": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.1.3.tgz", + "integrity": "sha512-r+pAcYtWdN8tKlYZPwiiHNA2QPjXnI02NrW5Sf2cVM3TRtuQ3V9EKKwOxqwaQ0krsaEXk/CbN90I5erBuf84Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/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/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "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", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "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", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "vendor/codex-ai-plugin": { + "name": "@codex-ai/plugin", + "version": "1.2.10-codex.1" + }, + "vendor/codex-ai-sdk": { + "name": "@codex-ai/sdk", + "version": "1.2.10-codex.1", + "dev": true + } + } } diff --git a/package.json b/package.json index 21fb17154..aaeba506a 100644 --- a/package.json +++ b/package.json @@ -154,15 +154,20 @@ "dependencies": { "@codex-ai/plugin": "file:vendor/codex-ai-plugin", "@openauthjs/openauth": "^0.4.3", - "hono": "4.12.6", + "hono": "4.12.10", "undici": "^6.24.1", "zod": "^4.3.6" }, "overrides": { - "hono": "4.12.6", + "hono": "4.12.10", + "flatted": "3.4.2", "minimatch": "10.2.4", + "picomatch": "4.0.4", "rollup": "4.59.0", "vite": "^7.3.1", + "micromatch": { + "picomatch": "2.3.2" + }, "@typescript-eslint/typescript-estree": { "minimatch": "9.0.9" } diff --git a/test/accounts.test.ts b/test/accounts.test.ts index 1fb49029b..7e0490e7f 100644 --- a/test/accounts.test.ts +++ b/test/accounts.test.ts @@ -3030,7 +3030,7 @@ describe("AccountManager", () => { expect(getAccountIdentityKey(rotatedAccount)).not.toBe(`${trackerKey}`); expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( degradedScore, - 6, + 5, ); expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); }); @@ -3085,7 +3085,7 @@ describe("AccountManager", () => { expect(getRuntimeTrackerKey(account)).toBe(trackerKey); expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( degradedScore, - 6, + 5, ); expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); }); From 788cb35d4b1ae3e48c4321d85a0c81fd0fa097b7 Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:33:31 +0800 Subject: [PATCH 02/18] test: cover windows delayed-save paths --- test/accounts.test.ts | 223 ++++++++++++++++++++++++++++-------------- 1 file changed, 150 insertions(+), 73 deletions(-) diff --git a/test/accounts.test.ts b/test/accounts.test.ts index 7e0490e7f..90933e340 100644 --- a/test/accounts.test.ts +++ b/test/accounts.test.ts @@ -1866,6 +1866,71 @@ describe("AccountManager", () => { } }); + it("uses the manager's captured Windows-style storage path state for delayed saves", async () => { + const { saveAccounts, withAccountStorageTransaction } = await import("../lib/storage.js"); + const mockSaveAccounts = vi.mocked(saveAccounts); + const mockWithAccountStorageTransaction = vi.mocked( + withAccountStorageTransaction, + ); + mockSaveAccounts.mockClear(); + + const repoAStoragePath = String.raw`C:\repo-a\storage.json`; + const repoARoot = String.raw`C:\repo-a`; + const repoBStoragePath = String.raw`C:\repo-b\storage.json`; + const repoBRoot = String.raw`C:\repo-b`; + + vi.useFakeTimers(); + try { + const now = Date.now(); + const stored = { + version: 3 as const, + activeIndex: 0, + accounts: [ + { refreshToken: "token-1", addedAt: now, lastUsed: now }, + ], + }; + + setStoragePathState({ + currentStoragePath: repoAStoragePath, + currentLegacyProjectStoragePath: null, + currentLegacyWorktreeStoragePath: null, + currentProjectRoot: repoARoot, + }); + const manager = new AccountManager(undefined, stored); + + const seenStates: Array> = []; + mockWithAccountStorageTransaction.mockImplementationOnce(async (handler) => { + seenStates.push({ ...getStoragePathState() }); + let current = null; + const persist = async (storage: Parameters[0]) => { + current = structuredClone(storage); + await mockSaveAccounts(storage); + }; + return handler(current as never, persist); + }); + + setStoragePathState({ + currentStoragePath: repoBStoragePath, + currentLegacyProjectStoragePath: null, + currentLegacyWorktreeStoragePath: null, + currentProjectRoot: repoBRoot, + }); + + manager.saveToDiskDebounced(50); + await vi.advanceTimersByTimeAsync(60); + + expect(seenStates).toEqual([ + expect.objectContaining({ + currentStoragePath: repoAStoragePath, + currentProjectRoot: repoARoot, + }), + ]); + expect(mockSaveAccounts).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + it("keeps ambient storage path state stable during concurrent saves", async () => { const { withAccountStorageTransaction } = await import("../lib/storage.js"); const mockWithAccountStorageTransaction = vi.mocked( @@ -3003,91 +3068,103 @@ describe("AccountManager", () => { }); it("keeps refresh-only tracker state stable when the refresh token rotates", () => { - const now = Date.now(); - const stored = { - version: 3 as const, - activeIndex: 0, - accounts: [ - { refreshToken: "token-1", addedAt: now, lastUsed: now }, - ], - }; + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); + try { + const now = Date.now(); + const stored = { + version: 3 as const, + activeIndex: 0, + accounts: [ + { refreshToken: "token-1", addedAt: now, lastUsed: now }, + ], + }; - const manager = new AccountManager(undefined, stored); - const account = manager.getCurrentAccount()!; - const healthTracker = getHealthTracker(); - const tokenTracker = getTokenTracker(); - const trackerKey = getRuntimeTrackerKey(account); + const manager = new AccountManager(undefined, stored); + const account = manager.getCurrentAccount()!; + const healthTracker = getHealthTracker(); + const tokenTracker = getTokenTracker(); + const trackerKey = getRuntimeTrackerKey(account); - manager.recordFailure(account, "codex", "gpt-5.1"); - const degradedScore = healthTracker.getScore(trackerKey, "codex:gpt-5.1"); - expect(manager.consumeToken(account, "codex", "gpt-5.1")).toBe(true); + manager.recordFailure(account, "codex", "gpt-5.1"); + const degradedScore = healthTracker.getScore(trackerKey, "codex:gpt-5.1"); + expect(manager.consumeToken(account, "codex", "gpt-5.1")).toBe(true); - account.refreshToken = "token-1-rotated"; + account.refreshToken = "token-1-rotated"; - const rotatedAccount = manager.getCurrentAccount()!; - expect(getRuntimeTrackerKey(rotatedAccount)).toBe(trackerKey); - expect(getRuntimeAccountIdentityKey(rotatedAccount)).toBe(trackerKey); - expect(getAccountIdentityKey(rotatedAccount)).not.toBe(`${trackerKey}`); - expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( - degradedScore, - 5, - ); - expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); + const rotatedAccount = manager.getCurrentAccount()!; + expect(getRuntimeTrackerKey(rotatedAccount)).toBe(trackerKey); + expect(getRuntimeAccountIdentityKey(rotatedAccount)).toBe(trackerKey); + expect(getAccountIdentityKey(rotatedAccount)).not.toBe(`${trackerKey}`); + expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( + degradedScore, + 6, + ); + expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); + } finally { + vi.useRealTimers(); + } }); it("keeps pinned runtime tracker state stable after updateFromAuth enriches identity", () => { - const now = Date.now(); - const stored = { - version: 3 as const, - activeIndex: 0, - accounts: [ - { refreshToken: "token-1", addedAt: now, lastUsed: now }, - { - refreshToken: "token-2", - email: "healthy@example.com", - addedAt: now, - lastUsed: now, - }, - ], - }; + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); + try { + const now = Date.now(); + const stored = { + version: 3 as const, + activeIndex: 0, + accounts: [ + { refreshToken: "token-1", addedAt: now, lastUsed: now }, + { + refreshToken: "token-2", + email: "healthy@example.com", + addedAt: now, + lastUsed: now, + }, + ], + }; - const manager = new AccountManager(undefined, stored); - const account = manager.getAccountByIndex(0)!; - const healthTracker = getHealthTracker(); - const tokenTracker = getTokenTracker(); - const trackerKey = getRuntimeTrackerKey(account); + const manager = new AccountManager(undefined, stored); + const account = manager.getAccountByIndex(0)!; + const healthTracker = getHealthTracker(); + const tokenTracker = getTokenTracker(); + const trackerKey = getRuntimeTrackerKey(account); - manager.recordFailure(account, "codex", "gpt-5.1"); - const degradedScore = healthTracker.getScore(trackerKey, "codex:gpt-5.1"); - expect(manager.consumeToken(account, "codex", "gpt-5.1")).toBe(true); + manager.recordFailure(account, "codex", "gpt-5.1"); + const degradedScore = healthTracker.getScore(trackerKey, "codex:gpt-5.1"); + expect(manager.consumeToken(account, "codex", "gpt-5.1")).toBe(true); - const payload = Buffer.from(JSON.stringify({ - email: "enriched@example.com", - "https://api.openai.com/auth": { - chatgpt_account_id: "account-enriched", - }, - exp: Math.floor((now + 3600000) / 1000), - })).toString("base64url"); - const accessToken = `header.${payload}.signature`; + const payload = Buffer.from(JSON.stringify({ + email: "enriched@example.com", + "https://api.openai.com/auth": { + chatgpt_account_id: "account-enriched", + }, + exp: Math.floor((now + 3600000) / 1000), + })).toString("base64url"); + const accessToken = `header.${payload}.signature`; - manager.updateFromAuth(account, { - type: "oauth", - access: accessToken, - refresh: "token-1-rotated", - expires: now + 3600000, - }); + manager.updateFromAuth(account, { + type: "oauth", + access: accessToken, + refresh: "token-1-rotated", + expires: now + 3600000, + }); - expect(account.accountId).toBe("account-enriched"); - expect(account.email).toBe("enriched@example.com"); - expect(getRuntimeAccountIdentityKey(account)).toBe( - "account:account-enriched::email:enriched@example.com", - ); - expect(getRuntimeTrackerKey(account)).toBe(trackerKey); - expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( - degradedScore, - 5, - ); - expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); + expect(account.accountId).toBe("account-enriched"); + expect(account.email).toBe("enriched@example.com"); + expect(getRuntimeAccountIdentityKey(account)).toBe( + "account:account-enriched::email:enriched@example.com", + ); + expect(getRuntimeTrackerKey(account)).toBe(trackerKey); + expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( + degradedScore, + 6, + ); + expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); + } finally { + vi.useRealTimers(); + } }); it("preserves tracker state when account indexes shift", () => { From 6fdf9cf86ad85954eed5541436495217f2236b73 Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:43:46 +0800 Subject: [PATCH 03/18] fix usage-limit cooldown persistence --- index.ts | 44 ++- lib/accounts.ts | 6 +- lib/preemptive-quota-scheduler.ts | 10 +- lib/request/fetch-helpers.ts | 212 +++++++++---- test/accounts.test.ts | 70 +++++ test/fetch-helpers.test.ts | 108 ++++++- test/index.test.ts | 383 +++++++++++++++++++++++- test/preemptive-quota-scheduler.test.ts | 17 ++ 8 files changed, 760 insertions(+), 90 deletions(-) diff --git a/index.ts b/index.ts index c26f6c7cf..df01ed8c3 100644 --- a/index.ts +++ b/index.ts @@ -1865,13 +1865,17 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { quotaKey, rateLimit.retryAfterMs, ); + const cooldownMs = Math.max( + delayMs, + rateLimit.retryAfterMs, + ); preemptiveQuotaScheduler.markRateLimited( quotaScheduleKey, - delayMs, + cooldownMs, ); - const waitLabel = formatWaitTime(delayMs); + const waitLabel = formatWaitTime(cooldownMs); - if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) { + if (cooldownMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) { if ( accountManager.shouldShowAccountToast( account.index, @@ -1887,14 +1891,14 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { } await sleep( - addJitter(Math.max(MIN_BACKOFF_MS, delayMs), 0.2), + addJitter(Math.max(MIN_BACKOFF_MS, cooldownMs), 0.2), ); continue; } accountManager.markRateLimitedWithReason( account, - delayMs, + cooldownMs, modelFamily, parseRateLimitReason(rateLimit.code), model, @@ -2142,19 +2146,29 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { ); } if (!fallbackResponse.ok) { - try { - await fallbackResponse.body?.cancel(); - } catch { - // Best effort cleanup before trying next fallback account. - } - if (fallbackResponse.status === 429) { + const { response: handledFallbackResponse, rateLimit: fallbackRateLimit } = + await handleErrorResponse(fallbackResponse); + if (handledFallbackResponse.status === 429) { const retryAfterMs = - parseRetryAfterHintMs( - fallbackResponse.headers, - ) ?? 60_000; + fallbackRateLimit?.retryAfterMs ?? 60_000; + const fallbackQuotaKey = + model ? `${modelFamily}:${model}` : modelFamily; + const { delayMs } = getRateLimitBackoff( + fallbackAccount.index, + fallbackQuotaKey, + retryAfterMs, + ); + const cooldownMs = Math.max( + delayMs, + retryAfterMs, + ); + preemptiveQuotaScheduler.markRateLimited( + `${fallbackEntitlementAccountKey}:${model ?? modelFamily}`, + cooldownMs, + ); accountManager.markRateLimitedWithReason( fallbackAccount, - retryAfterMs, + cooldownMs, modelFamily, "quota", model, diff --git a/lib/accounts.ts b/lib/accounts.ts index 844aac9b2..9b0e2dd24 100644 --- a/lib/accounts.ts +++ b/lib/accounts.ts @@ -783,7 +783,8 @@ export class AccountManager { const baseKey = getQuotaKey(family); if (!model || reason === "quota" || reason === "unknown") { - account.rateLimitResetTimes[baseKey] = resetAt; + const currentResetAt = account.rateLimitResetTimes[baseKey] ?? 0; + account.rateLimitResetTimes[baseKey] = Math.max(currentResetAt, resetAt); } if ( @@ -791,7 +792,8 @@ export class AccountManager { (reason === "tokens" || reason === "concurrent" || reason === "unknown") ) { const modelKey = getQuotaKey(family, model); - account.rateLimitResetTimes[modelKey] = resetAt; + const currentResetAt = account.rateLimitResetTimes[modelKey] ?? 0; + account.rateLimitResetTimes[modelKey] = Math.max(currentResetAt, resetAt); } account.lastRateLimitReason = reason; diff --git a/lib/preemptive-quota-scheduler.ts b/lib/preemptive-quota-scheduler.ts index e1bfe5285..4d8c22d07 100644 --- a/lib/preemptive-quota-scheduler.ts +++ b/lib/preemptive-quota-scheduler.ts @@ -212,14 +212,20 @@ export class PreemptiveQuotaScheduler { markRateLimited(key: string, retryAfterMs: number, now = Date.now()): void { if (!key) return; const waitMs = Number.isFinite(retryAfterMs) ? Math.max(0, Math.floor(retryAfterMs)) : 0; + const existing = this.snapshots.get(key); + const existingResetAtMs = Math.max( + existing?.primary.resetAtMs ?? 0, + existing?.secondary.resetAtMs ?? 0, + ); + const nextResetAtMs = Math.max(existingResetAtMs, now + waitMs); this.snapshots.set(key, { status: 429, primary: { usedPercent: 100, - resetAtMs: now + waitMs, + resetAtMs: nextResetAtMs, }, secondary: {}, - updatedAt: now, + updatedAt: Math.max(existing?.updatedAt ?? 0, now), }); } diff --git a/lib/request/fetch-helpers.ts b/lib/request/fetch-helpers.ts index e62241d2f..05808c14a 100644 --- a/lib/request/fetch-helpers.ts +++ b/lib/request/fetch-helpers.ts @@ -68,7 +68,11 @@ const CHATGPT_CODEX_UNSUPPORTED_MODEL_PATTERN = /model is not supported when using codex with a chatgpt account/i; const NORMALIZED_UNSUPPORTED_MODEL_PATTERN = /the model ['"]([^'"]+)['"] is not currently available for this chatgpt account/i; -const MAX_RETRY_DELAY_MS = 5 * 60 * 1000; +const MAX_RATE_LIMIT_DELAY_MS = 7 * 24 * 60 * 60 * 1000; +const RETRY_AFTER_DURATION_PATTERN = + /\b(?:try|retry)\s+again\s+in\s+(\d+)\s*(second|minute|hour|day)s?\b/i; +const RETRY_AFTER_CLOCK_TIME_PATTERN = + /\b(?:try|retry)\s+again\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i; const CREATE_CODEX_HEADERS_PARAM_KEYS = new Set(["init", "accountId", "accessToken", "opts"]); const DEFAULT_PROXY_PORTS: Record = { "http:": 80, @@ -1010,23 +1014,16 @@ function extractRateLimitInfoFromBody( const isStatusRateLimit = response.status === HTTP_STATUS.TOO_MANY_REQUESTS; const parsed = parseRateLimitBody(bodyText); - - const haystack = `${parsed?.code ?? ""} ${bodyText}`.toLowerCase(); // Entitlement errors should not be treated as rate limits if (isEntitlementError(parsed?.code ?? "", bodyText)) { return undefined; } - - const isRateLimit = - isStatusRateLimit || - /usage_limit_reached|rate_limit_exceeded|rate_limit|usage limit/i.test( - haystack, - ); - if (!isRateLimit) return undefined; + + if (!isStatusRateLimit) return undefined; const retryAfterMs = - parseRetryAfterMs(response, parsed) ?? 60000; + parseRetryAfterMs(response, bodyText, parsed) ?? 60000; return { retryAfterMs, code: parsed?.code }; } @@ -1197,8 +1194,24 @@ function ensureJsonErrorResponse(response: Response, payload: ErrorPayload): Res }); } +function parseResetTimestampMs(rawValue: string): number | null { + const trimmed = rawValue.trim(); + if (trimmed.length === 0) return null; + + if (/^\d+$/.test(trimmed)) { + const parsed = Number.parseInt(trimmed, 10); + if (Number.isFinite(parsed) && parsed > 0) { + return parsed < 10_000_000_000 ? parsed * 1000 : parsed; + } + } + + const parsedDate = Date.parse(trimmed); + return Number.isFinite(parsedDate) ? parsedDate : null; +} + function parseRetryAfterMs( response: Response, + bodyText: string, parsedBody?: { resetsAt?: number; retryAfterMs?: number; retryAfterSeconds?: number }, ): number | null { if (parsedBody?.retryAfterMs !== undefined) { @@ -1211,69 +1224,144 @@ function parseRetryAfterMs( if (normalized !== null) return normalized; } - const retryAfterMsHeader = response.headers.get("retry-after-ms"); - if (retryAfterMsHeader) { - const parsed = Number.parseInt(retryAfterMsHeader, 10); - if (!Number.isNaN(parsed) && parsed > 0) { - return parsed; - } - } + const retryAfterMsHeader = response.headers.get("retry-after-ms"); + if (retryAfterMsHeader) { + const parsed = Number.parseInt(retryAfterMsHeader, 10); + const normalized = normalizeRetryAfterMs(parsed); + if (normalized !== null) { + return normalized; + } + } - const retryAfterHeader = response.headers.get("retry-after"); - if (retryAfterHeader) { - const parsed = Number.parseInt(retryAfterHeader, 10); - if (!Number.isNaN(parsed) && parsed > 0) { - return parsed * 1000; - } - } + const retryAfterHeader = response.headers.get("retry-after"); + if (retryAfterHeader) { + const parsed = Number.parseInt(retryAfterHeader, 10); + const normalized = normalizeRetryAfterSeconds(parsed); + if (normalized !== null) { + return normalized; + } + } - const resetAtHeaders = [ - "x-codex-primary-reset-at", - "x-codex-secondary-reset-at", - "x-ratelimit-reset", - ]; - const now = Date.now(); - const resetCandidates: number[] = []; - for (const header of resetAtHeaders) { - const value = response.headers.get(header); - if (!value) continue; - const parsed = Number.parseInt(value, 10); - if (!Number.isNaN(parsed) && parsed > 0) { - const timestamp = - parsed < 10_000_000_000 ? parsed * 1000 : parsed; - const delta = timestamp - now; - if (delta > 0) resetCandidates.push(delta); - } - } + const resetAfterSecondsHeaders = [ + "x-codex-primary-reset-after-seconds", + "x-codex-secondary-reset-after-seconds", + ]; + const resetCandidates: number[] = []; + for (const header of resetAfterSecondsHeaders) { + const value = response.headers.get(header); + if (!value) continue; + const parsed = Number.parseInt(value, 10); + const normalized = normalizeRetryAfterSeconds(parsed); + if (normalized !== null) { + resetCandidates.push(normalized); + } + } - if (parsedBody?.resetsAt) { - const timestamp = - parsedBody.resetsAt < 10_000_000_000 - ? parsedBody.resetsAt * 1000 - : parsedBody.resetsAt; - const delta = timestamp - now; - if (delta > 0) resetCandidates.push(delta); - } + const resetAtHeaders = [ + "x-codex-primary-reset-at", + "x-codex-secondary-reset-at", + "x-ratelimit-reset", + ]; + const now = Date.now(); + for (const header of resetAtHeaders) { + const value = response.headers.get(header); + if (!value) continue; + const timestamp = parseResetTimestampMs(value); + if (timestamp === null) continue; + const delta = normalizeRetryAfterMs(timestamp - now); + if (delta !== null) resetCandidates.push(delta); + } - if (resetCandidates.length > 0) { - return Math.min(...resetCandidates); - } + if (parsedBody?.resetsAt) { + const timestamp = + parsedBody.resetsAt < 10_000_000_000 + ? parsedBody.resetsAt * 1000 + : parsedBody.resetsAt; + const delta = normalizeRetryAfterMs(timestamp - now); + if (delta !== null) resetCandidates.push(delta); + } + + if (resetCandidates.length > 0) { + return Math.max(...resetCandidates); + } + + const naturalLanguageRetryAfterMs = parseRetryAfterTextMs(bodyText, now); + if (naturalLanguageRetryAfterMs !== null) { + return naturalLanguageRetryAfterMs; + } - return null; + return null; } -function normalizeRetryAfterMs(value: number): number | null { +function parseRetryAfterTextMs(bodyText: string, now: number): number | null { + if (!bodyText) return null; + + const durationMatch = bodyText.match(RETRY_AFTER_DURATION_PATTERN); + if (durationMatch) { + const amount = Number.parseInt(durationMatch[1] ?? "", 10); + const unit = (durationMatch[2] ?? "").toLowerCase(); + if (Number.isFinite(amount) && amount > 0) { + const multiplier = + unit === "second" + ? 1000 + : unit === "minute" + ? 60_000 + : unit === "hour" + ? 60 * 60_000 + : unit === "day" + ? 24 * 60 * 60_000 + : 0; + if (multiplier > 0) { + return clampRateLimitDelayMs(amount * multiplier); + } + } + } + + const clockTimeMatch = bodyText.match(RETRY_AFTER_CLOCK_TIME_PATTERN); + if (!clockTimeMatch) return null; + + const hour12 = Number.parseInt(clockTimeMatch[1] ?? "", 10); + const minute = Number.parseInt(clockTimeMatch[2] ?? "0", 10); + const meridiem = (clockTimeMatch[3] ?? "").toLowerCase(); + if ( + !Number.isFinite(hour12) || + hour12 < 1 || + hour12 > 12 || + !Number.isFinite(minute) || + minute < 0 || + minute > 59 || + (meridiem !== "am" && meridiem !== "pm") + ) { + return null; + } + + const target = new Date(now); + let hour24 = hour12 % 12; + if (meridiem === "pm") { + hour24 += 12; + } + target.setHours(hour24, minute, 0, 0); + if (target.getTime() <= now) { + target.setDate(target.getDate() + 1); + } + + return clampRateLimitDelayMs(target.getTime() - now); +} + +function clampRateLimitDelayMs(value: number): number | null { if (!Number.isFinite(value)) return null; - const ms = Math.floor(value); - if (ms <= 0) return null; - return Math.min(ms, MAX_RETRY_DELAY_MS); + const normalized = Math.floor(value); + if (normalized <= 0) return null; + return Math.min(normalized, MAX_RATE_LIMIT_DELAY_MS); +} + +function normalizeRetryAfterMs(value: number): number | null { + return clampRateLimitDelayMs(value); } function normalizeRetryAfterSeconds(value: number): number | null { if (!Number.isFinite(value)) return null; - const ms = Math.floor(value * 1000); - if (ms <= 0) return null; - return Math.min(ms, MAX_RETRY_DELAY_MS); + return clampRateLimitDelayMs(value * 1000); } function toNumber(value: unknown): number | undefined { diff --git a/test/accounts.test.ts b/test/accounts.test.ts index 90933e340..93249b533 100644 --- a/test/accounts.test.ts +++ b/test/accounts.test.ts @@ -918,6 +918,76 @@ describe("AccountManager", () => { expect(account.rateLimitResetTimes["codex"]).toBeUndefined(); expect(account.rateLimitResetTimes["codex:gpt-5.2"]).toBeDefined(); }); + + it("does not shorten an existing reset when a later retry window is smaller", () => { + vi.useFakeTimers(); + try { + const now = new Date("2026-04-05T00:00:00.000Z"); + vi.setSystemTime(now); + const stored = { + version: 3 as const, + activeIndex: 0, + accounts: [ + { refreshToken: "token-1", addedAt: now.getTime(), lastUsed: now.getTime() }, + ], + }; + + const manager = new AccountManager(undefined, stored); + const account = manager.getCurrentAccount()!; + manager.markRateLimitedWithReason(account, 90 * 60_000, "codex", "quota"); + + const expectedResetAt = now.getTime() + 90 * 60_000; + + vi.advanceTimersByTime(30 * 60_000); + + manager.markRateLimitedWithReason(account, 60_000, "codex", "quota"); + + expect(account.rateLimitResetTimes["codex"]).toBe(expectedResetAt); + } finally { + vi.useRealTimers(); + } + }); + + it("does not shorten an existing model-scoped reset when a later retry window is smaller", () => { + vi.useFakeTimers(); + try { + const now = new Date("2026-04-05T00:00:00.000Z"); + vi.setSystemTime(now); + const stored = { + version: 3 as const, + activeIndex: 0, + accounts: [ + { refreshToken: "token-1", addedAt: now.getTime(), lastUsed: now.getTime() }, + ], + }; + + const manager = new AccountManager(undefined, stored); + const account = manager.getCurrentAccount()!; + manager.markRateLimitedWithReason( + account, + 90 * 60_000, + "codex", + "tokens", + "gpt-5.2", + ); + + const expectedResetAt = now.getTime() + 90 * 60_000; + + vi.advanceTimersByTime(30 * 60_000); + + manager.markRateLimitedWithReason( + account, + 60_000, + "codex", + "tokens", + "gpt-5.2", + ); + + expect(account.rateLimitResetTimes["codex:gpt-5.2"]).toBe(expectedResetAt); + } finally { + vi.useRealTimers(); + } + }); }); describe("cooldown management", () => { diff --git a/test/fetch-helpers.test.ts b/test/fetch-helpers.test.ts index 15313e9a2..a79b51514 100644 --- a/test/fetch-helpers.test.ts +++ b/test/fetch-helpers.test.ts @@ -990,6 +990,17 @@ describe('createEntitlementErrorResponse', () => { expect(rateLimit?.retryAfterMs).toBeGreaterThan(0); }); + it('does not treat non-429 rate-limit text as a cooldown signal', async () => { + const response = new Response('rate_limit_exceeded - upstream overloaded', { + status: 500, + }); + + const { response: result, rateLimit } = await handleErrorResponse(response); + + expect(result.status).toBe(500); + expect(rateLimit).toBeUndefined(); + }); + it('handles 429 with entitlement error code (should not be rate limit)', async () => { const body = { error: { code: 'usage_not_included', message: 'Not included' } }; const response = new Response(JSON.stringify(body), { status: 429 }); @@ -1084,6 +1095,27 @@ describe('createEntitlementErrorResponse', () => { expect(rateLimit?.retryAfterMs).toBeGreaterThan(0); }); + it('prefers the longest reset hint across reset-after-seconds and date-form reset-at headers', async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date('2026-04-05T00:00:00.000Z')); + const headers = new Headers({ + 'x-codex-primary-reset-after-seconds': '60', + 'x-codex-secondary-reset-at': new Date('2026-04-05T01:30:00.000Z').toUTCString(), + }); + const response = new Response( + JSON.stringify({ error: { message: 'rate limited' } }), + { status: 429, headers }, + ); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit?.retryAfterMs).toBe(90 * 60 * 1000); + } finally { + vi.useRealTimers(); + } + }); + it('keeps retry_after_ms values in milliseconds', async () => { const body = { error: { message: 'rate limited', retry_after_ms: 5 } }; const response = new Response(JSON.stringify(body), { status: 429 }); @@ -1112,15 +1144,85 @@ describe('createEntitlementErrorResponse', () => { expect(rateLimit?.retryAfterMs).toBe(2500); }); - it('caps retryAfterMs at 5 minutes', async () => { - const body = { error: { message: 'rate limited', retry_after_ms: 600000 } }; + it('parses natural-language retry times from usage-limit messages', async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date(2026, 2, 22, 4, 0, 0, 0)); + const response = new Response( + "You've hit your usage limit. To get more access now, send a request to your admin or try again at 6:26 AM.", + { status: 429 }, + ); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit?.retryAfterMs).toBe((2 * 60 + 26) * 60 * 1000); + } finally { + vi.useRealTimers(); + } + }); + + it('parses natural-language retry durations from usage-limit messages', async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date('2026-04-05T00:00:00.000Z')); + const response = new Response( + "You've hit your usage limit. Please try again in 2 hours.", + { status: 429 }, + ); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit?.retryAfterMs).toBe(2 * 60 * 60 * 1000); + } finally { + vi.useRealTimers(); + } + }); + + it('caps retryAfterMs at 7 days', async () => { + const body = { error: { message: 'rate limited', retry_after_ms: 10 * 24 * 60 * 60 * 1000 } }; const response = new Response(JSON.stringify(body), { status: 429 }); const { rateLimit } = await handleErrorResponse(response); - expect(rateLimit?.retryAfterMs).toBe(300000); + expect(rateLimit?.retryAfterMs).toBe(7 * 24 * 60 * 60 * 1000); }); + it('caps retry-after-ms header values at 7 days', async () => { + const headers = new Headers({ + 'retry-after-ms': String(10 * 24 * 60 * 60 * 1000), + }); + const response = new Response( + JSON.stringify({ error: { message: 'rate limited' } }), + { status: 429, headers }, + ); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit?.retryAfterMs).toBe(7 * 24 * 60 * 60 * 1000); + }); + + it('caps reset timestamp hints at 7 days', async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date('2026-04-05T00:00:00.000Z')); + const resetAtSeconds = + Math.floor(Date.now() / 1000) + 10 * 24 * 60 * 60; + const headers = new Headers({ + 'x-ratelimit-reset': String(resetAtSeconds), + }); + const response = new Response( + JSON.stringify({ error: { message: 'rate limited' } }), + { status: 429, headers }, + ); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit?.retryAfterMs).toBe(7 * 24 * 60 * 60 * 1000); + } finally { + vi.useRealTimers(); + } + }); + it('handles invalid retry-after header with default fallback', async () => { const headers = new Headers({ 'retry-after': 'invalid' }); const response = new Response(JSON.stringify({ error: { message: 'rate limited' } }), { status: 429, headers }); diff --git a/test/index.test.ts b/test/index.test.ts index c8d18e19d..cbc71ec2f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -454,10 +454,6 @@ vi.mock("../lib/accounts.js", async () => { markToastShown() {} - getCurrentWorkspace() { - return null; - } - disableCurrentWorkspace() { return false; } @@ -2902,7 +2898,6 @@ describe("OpenAIOAuthPlugin fetch handler", () => { getMinWaitTimeForFamily: () => 0, shouldShowAccountToast: () => false, markToastShown: () => {}, - getCurrentWorkspace: () => null, disableCurrentWorkspace: () => false, rotateToNextWorkspace: () => null, hasEnabledWorkspaces: () => true, @@ -4140,7 +4135,10 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { vi.spyOn(AccountManager, "loadFromDisk").mockResolvedValue(manager as never); vi.mocked(fetchHelpersModule.handleErrorResponse).mockResolvedValueOnce({ response: new Response("rate limited", { status: 429 }), - rateLimit: true, + rateLimit: { + retryAfterMs: 1000, + code: "usage_limit_reached", + }, errorBody: "rate limited", } as never); vi.mocked(rateLimitBackoffModule.getRateLimitBackoff).mockReturnValueOnce({ @@ -4176,6 +4174,379 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { expect(markToastShown).toHaveBeenCalledWith(0); }); + it("does not short-retry the same account when the parsed cooldown exceeds the retry floor", async () => { + const { AccountManager } = await import("../lib/accounts.js"); + const fetchHelpersModule = await import("../lib/request/fetch-helpers.js"); + const rateLimitBackoffModule = await import("../lib/request/rate-limit-backoff.js"); + + const markRateLimitedWithReason = vi.fn(); + const manager = { + getAccountCount: () => 1, + getCurrentOrNextForFamilyHybrid: () => ({ + index: 0, + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + }), + getCurrentOrNextForFamily: () => ({ + index: 0, + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + }), + getCurrentWorkspace: () => null, + getAccountByIndex: () => null, + getAccountsSnapshot: () => [], + isAccountAvailableForFamily: () => true, + toAuthDetails: () => ({ + type: "oauth" as const, + access: "access-token", + refresh: "refresh-1", + expires: Date.now() + 60_000, + }), + hasRefreshToken: () => true, + saveToDiskDebounced: () => {}, + updateFromAuth: () => {}, + clearAuthFailures: () => {}, + incrementAuthFailures: () => 1, + saveToDisk: async () => {}, + markAccountCoolingDown: () => {}, + markRateLimited: () => {}, + markRateLimitedWithReason, + consumeToken: () => true, + refundToken: () => {}, + syncCodexCliActiveSelectionForIndex: async () => {}, + markSwitched: () => {}, + removeAccount: () => {}, + recordFailure: () => {}, + recordSuccess: () => {}, + recordRateLimit: () => {}, + getMinWaitTimeForFamily: () => 0, + shouldShowAccountToast: () => true, + markToastShown: () => {}, + setActiveIndex: () => null, + }; + vi.spyOn(AccountManager, "loadFromDisk").mockResolvedValue(manager as never); + vi.mocked(fetchHelpersModule.handleErrorResponse).mockResolvedValueOnce({ + response: new Response("rate limited", { status: 429 }), + rateLimit: { + retryAfterMs: 2 * 60 * 60 * 1000, + code: "usage_limit_reached", + }, + errorBody: "rate limited", + } as never); + vi.mocked(rateLimitBackoffModule.getRateLimitBackoff).mockReturnValueOnce({ + attempt: 2, + delayMs: 1000, + }); + globalThis.fetch = vi + .fn() + .mockResolvedValueOnce(new Response("rate limited", { status: 429 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ content: "ok" }), { status: 200 })); + + const mockClient = createMockClient(); + const { OpenAIOAuthPlugin } = await import("../index.js"); + const plugin = await OpenAIOAuthPlugin({ client: mockClient } as never) as unknown as PluginType; + const sdk = await plugin.auth.loader(getOAuthAuth, { options: {}, models: {} }); + showRuntimeToastMock.mockClear(); + const response = await sdk.fetch!("https://api.openai.com/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ model: "gpt-5.1" }), + }); + + expect(response.status).toBe(503); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + expect(markRateLimitedWithReason.mock.calls[0]?.[1]).toBe(2 * 60 * 60 * 1000); + expect(showRuntimeToastMock).not.toHaveBeenCalled(); + }); + + it("persists the longer parsed rate-limit cooldown across overlapping requests", async () => { + const { AccountManager } = await import("../lib/accounts.js"); + const fetchHelpersModule = await import("../lib/request/fetch-helpers.js"); + const rateLimitBackoffModule = await import("../lib/request/rate-limit-backoff.js"); + const longCooldownMs = 90 * 60 * 1000; + const shortCooldownMs = 60_000; + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date("2026-04-05T00:00:00.000Z")); + const requestAccount = { + index: 0, + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + rateLimitResetTimes: {} as Record, + lastSwitchReason: undefined as string | undefined, + }; + let observedBaseResetAt: number | undefined; + const markRateLimitedWithReason = vi.fn( + ( + account: { rateLimitResetTimes: Record }, + retryAfterMs: number, + family: string, + reason: string, + model?: string | null, + ) => { + const resetAt = Date.now() + retryAfterMs; + const baseKey = family; + if (!model || reason === "quota" || reason === "unknown") { + account.rateLimitResetTimes[baseKey] = Math.max( + account.rateLimitResetTimes[baseKey] ?? 0, + resetAt, + ); + observedBaseResetAt = account.rateLimitResetTimes[baseKey]; + } + if ( + model && + (reason === "tokens" || reason === "concurrent" || reason === "unknown") + ) { + const modelKey = `${family}:${model}`; + account.rateLimitResetTimes[modelKey] = Math.max( + account.rateLimitResetTimes[modelKey] ?? 0, + resetAt, + ); + } + }, + ); + const manager = { + getAccountCount: () => 1, + getCurrentOrNextForFamilyHybrid: () => requestAccount, + getCurrentOrNextForFamily: () => requestAccount, + getCurrentWorkspace: () => null, + getAccountByIndex: () => null, + getAccountsSnapshot: () => [requestAccount], + isAccountAvailableForFamily: () => true, + toAuthDetails: () => ({ + type: "oauth" as const, + access: "access-token", + refresh: "refresh-1", + expires: Date.now() + 60_000, + }), + hasRefreshToken: () => true, + saveToDiskDebounced: () => {}, + updateFromAuth: () => {}, + clearAuthFailures: () => {}, + incrementAuthFailures: () => 1, + saveToDisk: async () => {}, + markAccountCoolingDown: () => {}, + markRateLimited: () => {}, + markRateLimitedWithReason, + consumeToken: () => true, + refundToken: () => {}, + syncCodexCliActiveSelectionForIndex: async () => {}, + markSwitched: () => {}, + removeAccount: () => {}, + recordFailure: () => {}, + recordSuccess: () => {}, + recordRateLimit: () => {}, + getMinWaitTimeForFamily: () => 0, + shouldShowAccountToast: () => false, + markToastShown: () => {}, + setActiveIndex: () => null, + }; + vi.spyOn(AccountManager, "loadFromDisk").mockResolvedValue(manager as never); + + let rateLimitCallCount = 0; + const waitForSecondRateLimitCall = async () => { + for (let spin = 0; spin < 200; spin += 1) { + if (rateLimitCallCount > 1) { + return; + } + await Promise.resolve(); + } + throw new Error( + "second overlapping request never reached the rate-limit handler", + ); + }; + vi.mocked(fetchHelpersModule.handleErrorResponse).mockImplementation( + async () => { + const callIndex = rateLimitCallCount++; + const result = { + response: new Response("rate limited", { status: 429 }), + rateLimit: + callIndex === 0 + ? { + retryAfterMs: longCooldownMs, + code: "usage_limit_reached", + } + : { + retryAfterMs: shortCooldownMs, + code: "usage_limit_reached", + }, + errorBody: "rate limited", + }; + + if (callIndex === 0) { + await waitForSecondRateLimitCall(); + return result as never; + } + + await Promise.resolve(); + return result as never; + }, + ); + vi.mocked(rateLimitBackoffModule.getRateLimitBackoff).mockReturnValue({ + attempt: 1, + delayMs: 60_000, + }); + + let upstreamCallCount = 0; + globalThis.fetch = vi.fn(async () => { + upstreamCallCount += 1; + if (upstreamCallCount <= 2) { + return new Response("rate limited", { status: 429 }); + } + return new Response(JSON.stringify({ content: "ok" }), { status: 200 }); + }); + + const mockClient = createMockClient(); + const { OpenAIOAuthPlugin } = await import("../index.js"); + const plugin = await OpenAIOAuthPlugin({ client: mockClient } as never) as unknown as PluginType; + const sdk = await plugin.auth.loader(getOAuthAuth, { options: {}, models: {} }); + + await Promise.all([ + sdk.fetch!("https://api.openai.com/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ model: "gpt-5-codex" }), + }), + sdk.fetch!("https://api.openai.com/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ model: "gpt-5-codex" }), + }), + ]); + + expect(markRateLimitedWithReason).toHaveBeenCalledTimes(2); + expect(observedBaseResetAt).toBe( + Date.parse("2026-04-05T01:30:00.000Z"), + ); + } finally { + vi.useRealTimers(); + } + }); + + it("uses the floored cooldown for stream failover 429s", async () => { + const { AccountManager } = await import("../lib/accounts.js"); + const fetchHelpersModule = await import("../lib/request/fetch-helpers.js"); + const rateLimitBackoffModule = await import("../lib/request/rate-limit-backoff.js"); + const streamFailoverModule = await import("../lib/request/stream-failover.js"); + const currentAccount = { + index: 0, + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + accessToken: "access-alpha", + }; + const fallbackAccount = { + index: 1, + accountId: "acc-2", + email: "beta@example.com", + refreshToken: "refresh-2", + accessToken: "access-beta", + }; + const markRateLimitedWithReason = vi.fn(); + const recordRateLimit = vi.fn(); + const pendingFailovers: Array> = []; + const manager = { + getAccountCount: () => 2, + getCurrentOrNextForFamilyHybrid: () => currentAccount, + getCurrentOrNextForFamily: () => currentAccount, + getCurrentWorkspace: () => null, + getAccountByIndex: (index: number) => + index === fallbackAccount.index ? fallbackAccount : currentAccount, + getAccountsSnapshot: () => [currentAccount, fallbackAccount], + isAccountAvailableForFamily: (index: number) => index === fallbackAccount.index, + toAuthDetails: (account: typeof currentAccount | typeof fallbackAccount) => ({ + type: "oauth" as const, + access: account.accessToken, + refresh: account.refreshToken, + expires: Date.now() + 60_000, + }), + hasRefreshToken: () => true, + saveToDiskDebounced: () => {}, + updateFromAuth: () => {}, + clearAuthFailures: () => {}, + incrementAuthFailures: () => 1, + saveToDisk: async () => {}, + markAccountCoolingDown: () => {}, + markRateLimited: () => {}, + markRateLimitedWithReason, + consumeToken: () => true, + refundToken: () => {}, + syncCodexCliActiveSelectionForIndex: async () => {}, + markSwitched: () => {}, + removeAccount: () => {}, + recordFailure: () => {}, + recordSuccess: () => {}, + recordRateLimit, + getMinWaitTimeForFamily: () => 0, + shouldShowAccountToast: () => false, + markToastShown: () => {}, + setActiveIndex: () => null, + }; + vi.spyOn(AccountManager, "loadFromDisk").mockResolvedValueOnce(manager as never); + vi.mocked(fetchHelpersModule.handleErrorResponse).mockImplementation( + async (response: Response) => + ({ + response, + rateLimit: + response.status === 429 + ? { retryAfterMs: 4_000, code: "usage_limit_reached" } + : undefined, + errorBody: "rate limited", + }) as never, + ); + vi.mocked(fetchHelpersModule.transformRequestForCodex).mockImplementation( + async (init, _url, _userConfig, _codexMode, body) => ({ + updatedInit: { + ...(init as RequestInit), + body: JSON.stringify(body ?? {}), + }, + body: (body ?? { + model: "gpt-5.1", + stream: true, + }) as { + model: string; + stream?: boolean; + }, + }), + ); + vi.mocked(rateLimitBackoffModule.getRateLimitBackoff).mockReturnValue({ + attempt: 3, + delayMs: 15_000, + }); + vi.spyOn(streamFailoverModule, "withStreamingFailover").mockImplementation( + (initialResponse, getFallbackResponse) => { + pendingFailovers.push(getFallbackResponse(1, 0)); + return initialResponse; + }, + ); + let fetchCount = 0; + globalThis.fetch = vi.fn().mockImplementation(async () => { + fetchCount += 1; + if (fetchCount === 1) { + return new Response("data: ok\n\n", { status: 200 }); + } + return new Response("rate limited", { status: 429 }); + }); + + const mockClient = createMockClient(); + const { OpenAIOAuthPlugin } = await import("../index.js"); + const plugin = await OpenAIOAuthPlugin({ client: mockClient } as never) as unknown as PluginType; + const sdk = await plugin.auth.loader(getOAuthAuth, { options: {}, models: {} }); + const response = await sdk.fetch!("https://api.openai.com/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ model: "gpt-5.1", stream: true }), + }); + await Promise.all(pendingFailovers); + + expect(response.status).toBe(200); + expect(markRateLimitedWithReason.mock.calls[0]?.[1]).toBe(15_000); + expect(recordRateLimit).toHaveBeenCalledWith( + fallbackAccount, + "gpt-5.1", + "gpt-5.1", + ); + }); + it("forwards persistence error toast arguments through manual OAuth flow", async () => { const authModule = await import("../lib/auth/auth.js"); const storageModule = await import("../lib/storage.js"); diff --git a/test/preemptive-quota-scheduler.test.ts b/test/preemptive-quota-scheduler.test.ts index 1d58db8cb..3a0513e48 100644 --- a/test/preemptive-quota-scheduler.test.ts +++ b/test/preemptive-quota-scheduler.test.ts @@ -70,6 +70,23 @@ describe("preemptive quota scheduler", () => { expect(decision.waitMs).toBeGreaterThan(0); }); + it("preserves the longest known rate-limit reset across overlapping updates", () => { + const scheduler = new PreemptiveQuotaScheduler(); + scheduler.markRateLimited("acc:model", 30_000, 1_000); + scheduler.update("acc:model", { + status: 429, + primary: {}, + secondary: { resetAtMs: 31_000 }, + updatedAt: 1_000, + }); + scheduler.markRateLimited("acc:model", 10_000, 5_000); + + const decision = scheduler.getDeferral("acc:model", 6_000); + expect(decision.defer).toBe(true); + expect(decision.reason).toBe("rate-limit"); + expect(decision.waitMs).toBe(25_000); + }); + it("sanitizes non-finite retry-after values", () => { const scheduler = new PreemptiveQuotaScheduler(); scheduler.markRateLimited("acc:model", Number.NaN, 1_000); From eeb91ddc3f3e19e03c80444ea85d4a5dfc56e5fc Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:43:46 +0800 Subject: [PATCH 04/18] fix ready-first account ordering regressions --- lib/codex-manager.ts | 95 ++--- lib/forecast.ts | 22 +- lib/runtime/account-state.ts | 57 +-- test/account-status.test.ts | 39 ++ test/codex-manager-cli.test.ts | 628 +++++++++++++++++++++++++++++++++ 5 files changed, 722 insertions(+), 119 deletions(-) diff --git a/lib/codex-manager.ts b/lib/codex-manager.ts index 192330bb3..f2cb284d8 100644 --- a/lib/codex-manager.ts +++ b/lib/codex-manager.ts @@ -78,6 +78,10 @@ import { getModelProfile, resolveNormalizedModel, } from "./request/helpers/model-map.js"; +import { + formatRateLimitEntry as formatAccountRateLimitEntry, + resolveActiveIndex, +} from "./runtime/account-status.js"; import { loadQuotaCache, type QuotaCacheData, @@ -439,50 +443,12 @@ const IMPLEMENTED_FEATURES: ImplementedFeature[] = [ { id: 41, name: "Auto-switch to best account command" }, ]; -function resolveActiveIndex( - storage: AccountStorageV3, - family: ModelFamily = "codex", -): number { - const total = storage.accounts.length; - if (total === 0) return 0; - const rawCandidate = - storage.activeIndexByFamily?.[family] ?? storage.activeIndex; - const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0; - return Math.max(0, Math.min(raw, total - 1)); -} - -function getRateLimitResetTimeForFamily( - account: { rateLimitResetTimes?: Record }, - now: number, - family: ModelFamily, -): number | null { - const times = account.rateLimitResetTimes; - if (!times) return null; - - let minReset: number | null = null; - const prefix = `${family}:`; - for (const [key, value] of Object.entries(times)) { - if (typeof value !== "number") continue; - if (value <= now) continue; - if (key !== family && !key.startsWith(prefix)) continue; - if (minReset === null || value < minReset) { - minReset = value; - } - } - - return minReset; -} - function formatRateLimitEntry( account: { rateLimitResetTimes?: Record }, now: number, family: ModelFamily = "codex", ): string | null { - const resetAt = getRateLimitResetTimeForFamily(account, now, family); - if (typeof resetAt !== "number") return null; - const remaining = resetAt - now; - if (remaining <= 0) return null; - return `resets in ${formatWaitTime(remaining)}`; + return formatAccountRateLimitEntry(account, now, formatWaitTime, family); } function normalizeQuotaEmail(email: string | undefined): string | null { @@ -983,6 +949,13 @@ function readQuotaLeftPercent( return parseLeftPercentFromQuotaSummary(account.quotaSummary, windowLabel); } +function readQuotaFloorPercent(account: ExistingAccountInfo): number { + return Math.min( + readQuotaLeftPercent(account, "5h"), + readQuotaLeftPercent(account, "7d"), + ); +} + function accountStatusSortBucket( status: ExistingAccountInfo["status"], ): number { @@ -1004,10 +977,25 @@ function accountStatusSortBucket( } } +function accountReadinessSortBucket(account: ExistingAccountInfo): number { + const statusBucket = accountStatusSortBucket(account.status); + if (statusBucket >= 2) return statusBucket; + return account.quotaRateLimited ? 2 : statusBucket; +} + function compareReadyFirstAccounts( left: ExistingAccountInfo, right: ExistingAccountInfo, ): number { + const bucketDelta = + accountReadinessSortBucket(left) - + accountReadinessSortBucket(right); + if (bucketDelta !== 0) return bucketDelta; + + const leftFloor = readQuotaFloorPercent(left); + const rightFloor = readQuotaFloorPercent(right); + if (leftFloor !== rightFloor) return rightFloor - leftFloor; + const left5h = readQuotaLeftPercent(left, "5h"); const right5h = readQuotaLeftPercent(right, "5h"); if (left5h !== right5h) return right5h - left5h; @@ -1016,11 +1004,6 @@ function compareReadyFirstAccounts( const right7d = readQuotaLeftPercent(right, "7d"); if (left7d !== right7d) return right7d - left7d; - const bucketDelta = - accountStatusSortBucket(left.status) - - accountStatusSortBucket(right.status); - if (bucketDelta !== 0) return bucketDelta; - const leftLastUsed = left.lastUsed ?? 0; const rightLastUsed = right.lastUsed ?? 0; if (leftLastUsed !== rightLastUsed) return rightLastUsed - leftLastUsed; @@ -2590,6 +2573,7 @@ async function runAuthLogin(args: string[]): Promise { setStoragePath(null); let pendingMenuQuotaRefresh: Promise | null = null; let menuQuotaRefreshStatus: string | undefined; + let skipNextMenuQuotaAutoRefresh = false; loginFlow: while (true) { let existingStorage = await loadAccounts(); if (existingStorage && existingStorage.accounts.length > 0) { @@ -2607,7 +2591,16 @@ async function runAuthLogin(args: string[]): Promise { const showFetchStatus = displaySettings.menuShowFetchStatus ?? true; const quotaTtlMs = displaySettings.menuQuotaTtlMs ?? DEFAULT_MENU_QUOTA_REFRESH_TTL_MS; - if (shouldAutoFetchLimits && !pendingMenuQuotaRefresh) { + const shouldSkipAutoFetchThisPass = + !pendingMenuQuotaRefresh && skipNextMenuQuotaAutoRefresh; + if (shouldSkipAutoFetchThisPass) { + skipNextMenuQuotaAutoRefresh = false; + } + if ( + shouldAutoFetchLimits + && !pendingMenuQuotaRefresh + && !shouldSkipAutoFetchThisPass + ) { const staleCount = countMenuQuotaRefreshTargets( currentStorage, quotaCache, @@ -2626,7 +2619,10 @@ async function runAuthLogin(args: string[]): Promise { menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [${current}/${total}]`; }, ) - .then(() => undefined) + .then(() => { + skipNextMenuQuotaAutoRefresh = true; + return undefined; + }) .catch(() => undefined) .finally(() => { menuQuotaRefreshStatus = undefined; @@ -2651,6 +2647,7 @@ async function runAuthLogin(args: string[]): Promise { return 0; } if (menuResult.mode === "check") { + skipNextMenuQuotaAutoRefresh = false; await runActionPanel( "Quick Check", "Checking local session + live status", @@ -2662,6 +2659,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "deep-check") { + skipNextMenuQuotaAutoRefresh = false; await runActionPanel( "Deep Check", "Refreshing and testing all accounts", @@ -2673,6 +2671,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "forecast") { + skipNextMenuQuotaAutoRefresh = false; await runActionPanel( "Best Account", "Comparing accounts", @@ -2684,6 +2683,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "fix") { + skipNextMenuQuotaAutoRefresh = false; await runActionPanel( "Auto-Fix", "Checking and fixing common issues", @@ -2695,10 +2695,12 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "settings") { + skipNextMenuQuotaAutoRefresh = false; await configureUnifiedSettings(displaySettings); continue; } if (menuResult.mode === "verify-flagged") { + skipNextMenuQuotaAutoRefresh = false; await runActionPanel( "Problem Account Check", "Checking problem accounts", @@ -2710,6 +2712,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "fresh" && menuResult.deleteAll) { + skipNextMenuQuotaAutoRefresh = false; await runActionPanel( "Reset Accounts", "Deleting all saved accounts", diff --git a/lib/forecast.ts b/lib/forecast.ts index 146defdee..8531291ce 100644 --- a/lib/forecast.ts +++ b/lib/forecast.ts @@ -1,5 +1,6 @@ import { formatAccountLabel, formatWaitTime } from "./accounts.js"; import type { CodexQuotaSnapshot } from "./quota-probe.js"; +import { getRateLimitResetTimeForFamily } from "./runtime/account-status.js"; import type { AccountMetadataV3 } from "./storage.js"; import type { TokenFailure } from "./types.js"; @@ -107,27 +108,6 @@ function getRiskLevel(score: number): ForecastRiskLevel { return "low"; } -function getRateLimitResetTimeForFamily( - account: AccountMetadataV3, - now: number, - family = "codex", -): number | null { - const times = account.rateLimitResetTimes; - if (!times) return null; - - let minReset: number | null = null; - const prefix = `${family}:`; - for (const [key, value] of Object.entries(times)) { - if (typeof value !== "number") continue; - if (value <= now) continue; - if (key !== family && !key.startsWith(prefix)) continue; - if (minReset === null || value < minReset) { - minReset = value; - } - } - return minReset; -} - function getLiveQuotaWaitMs(snapshot: CodexQuotaSnapshot, now: number): number { const waits: number[] = []; for (const resetAt of [ diff --git a/lib/runtime/account-state.ts b/lib/runtime/account-state.ts index 7f6350e15..e8f1177e5 100644 --- a/lib/runtime/account-state.ts +++ b/lib/runtime/account-state.ts @@ -1,52 +1,5 @@ -import { formatWaitTime } from "../accounts.js"; -import type { ModelFamily } from "../prompts/codex.js"; - -export function resolveActiveIndex( - storage: { - activeIndex: number; - activeIndexByFamily?: Partial>; - accounts: unknown[]; - }, - family: ModelFamily = "codex", -): number { - const total = storage.accounts.length; - if (total === 0) return 0; - const rawCandidate = - storage.activeIndexByFamily?.[family] ?? storage.activeIndex; - const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0; - return Math.max(0, Math.min(raw, total - 1)); -} - -export function getRateLimitResetTimeForFamily( - account: { rateLimitResetTimes?: Record }, - now: number, - family: ModelFamily, -): number | null { - const times = account.rateLimitResetTimes; - if (!times) return null; - - let minReset: number | null = null; - const prefix = `${family}:`; - for (const [key, value] of Object.entries(times)) { - if (typeof value !== "number") continue; - if (value <= now) continue; - if (key !== family && !key.startsWith(prefix)) continue; - if (minReset === null || value < minReset) { - minReset = value; - } - } - - return minReset; -} - -export function formatRateLimitEntry( - account: { rateLimitResetTimes?: Record }, - now: number, - family: ModelFamily = "codex", -): string | null { - const resetAt = getRateLimitResetTimeForFamily(account, now, family); - if (typeof resetAt !== "number") return null; - const remaining = resetAt - now; - if (remaining <= 0) return null; - return `resets in ${formatWaitTime(remaining)}`; -} +export { + formatRateLimitEntry, + getRateLimitResetTimeForFamily, + resolveActiveIndex, +} from "./account-status.js"; diff --git a/test/account-status.test.ts b/test/account-status.test.ts index 668e6da47..b4313d24b 100644 --- a/test/account-status.test.ts +++ b/test/account-status.test.ts @@ -4,6 +4,11 @@ import { getRateLimitResetTimeForFamily, resolveActiveIndex, } from "../lib/runtime/account-status.js"; +import { + formatRateLimitEntry as formatRateLimitEntryFromBarrel, + getRateLimitResetTimeForFamily as getRateLimitResetTimeForFamilyFromBarrel, + resolveActiveIndex as resolveActiveIndexFromBarrel, +} from "../lib/runtime/account-state.js"; describe("account status helpers", () => { it("resolves active index using family overrides and clamps bounds", () => { @@ -61,4 +66,38 @@ describe("account status helpers", () => { ), ).toBeNull(); }); + + it("re-exports account status helpers through the account-state barrel", () => { + expect(resolveActiveIndexFromBarrel).toBe(resolveActiveIndex); + expect(getRateLimitResetTimeForFamilyFromBarrel).toBe( + getRateLimitResetTimeForFamily, + ); + expect(formatRateLimitEntryFromBarrel).toBe(formatRateLimitEntry); + + expect( + resolveActiveIndexFromBarrel( + { + activeIndex: 4, + activeIndexByFamily: { codex: 1 }, + accounts: [1, 2, 3], + }, + "codex", + ), + ).toBe(1); + expect( + getRateLimitResetTimeForFamilyFromBarrel( + { rateLimitResetTimes: { codex: 4_000 } }, + 1_000, + "codex", + ), + ).toBe(4_000); + expect( + formatRateLimitEntryFromBarrel( + { rateLimitResetTimes: { codex: 5_000 } }, + 1_000, + (ms) => `${ms}ms`, + "codex", + ), + ).toBe("resets in 4000ms"); + }); }); diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index 01f8c0a98..c91d78dc0 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -323,6 +323,39 @@ function createDeferred(): { return { promise, resolve, reject }; } +type ReadyFirstMenuSettings = { + showPerAccountRows: boolean; + showQuotaDetails: boolean; + showForecastReasons: boolean; + showRecommendations: boolean; + showLiveProbeNotes: boolean; + menuAutoFetchLimits: boolean; + menuSortEnabled: boolean; + menuSortMode: "ready-first"; + menuSortPinCurrent: boolean; + menuSortQuickSwitchVisibleRow: boolean; + menuQuotaTtlMs?: number; + menuShowFetchStatus?: boolean; +}; + +function createReadyFirstMenuSettings( + overrides: Partial = {}, +): ReadyFirstMenuSettings { + return { + showPerAccountRows: true, + showQuotaDetails: true, + showForecastReasons: true, + showRecommendations: true, + showLiveProbeNotes: true, + menuAutoFetchLimits: false, + menuSortEnabled: true, + menuSortMode: "ready-first", + menuSortPinCurrent: false, + menuSortQuickSwitchVisibleRow: true, + ...overrides, + }; +} + function cloneValue(value: T): T { return structuredClone(value); } @@ -6772,6 +6805,601 @@ describe("codex manager cli commands", () => { expect(firstCallAccounts[1]?.isCurrentAccount).toBe(true); }); + it("keeps ready accounts ahead of degraded limit rows in ready-first sorting", async () => { + const now = Date.now(); + loadAccountsMock.mockResolvedValue({ + version: 3, + activeIndex: 3, + activeIndexByFamily: { codex: 3 }, + accounts: [ + { + email: "cached-limit@example.com", + accountId: "acc_cached_limit", + refreshToken: "refresh-cached-limit", + accessToken: "access-cached-limit", + expiresAt: now + 3_600_000, + addedAt: now - 5_000, + lastUsed: now - 5_000, + enabled: true, + }, + { + email: "cooldown@example.com", + accountId: "acc_cooldown", + refreshToken: "refresh-cooldown", + accessToken: "access-cooldown", + expiresAt: now + 3_600_000, + addedAt: now - 4_000, + lastUsed: now - 4_000, + enabled: true, + coolingDownUntil: now + 90_000, + }, + { + email: "healthy-low@example.com", + accountId: "acc_healthy_low", + refreshToken: "refresh-healthy-low", + accessToken: "access-healthy-low", + expiresAt: now + 3_600_000, + addedAt: now - 3_000, + lastUsed: now - 3_000, + enabled: true, + }, + { + email: "healthy-high@example.com", + accountId: "acc_healthy_high", + refreshToken: "refresh-healthy-high", + accessToken: "access-healthy-high", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "rate-limited@example.com", + accountId: "acc_rate_limited", + refreshToken: "refresh-rate-limited", + accessToken: "access-rate-limited", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + rateLimitResetTimes: { codex: now + 60_000 }, + }, + ], + }); + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings(), + ); + loadQuotaCacheMock.mockResolvedValue({ + byAccountId: {}, + byEmail: { + "cached-limit@example.com": { + updatedAt: now, + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "cooldown@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "healthy-low@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 75, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 75, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "healthy-high@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 10, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 10, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "rate-limited@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 5, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 5, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }); + promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + const firstCallAccounts = promptLoginModeMock.mock.calls[0]?.[0] as Array<{ + email?: string; + sourceIndex?: number; + quotaRateLimited?: boolean; + }>; + expect(firstCallAccounts.map((account) => account.email)).toEqual([ + "healthy-high@example.com", + "healthy-low@example.com", + "cached-limit@example.com", + "rate-limited@example.com", + "cooldown@example.com", + ]); + expect(firstCallAccounts.map((account) => account.sourceIndex)).toEqual([ + 3, 2, 0, 4, 1, + ]); + expect(firstCallAccounts[2]?.quotaRateLimited).toBe(true); + }); + + it("keeps exhausted weekly quota below balanced ready accounts in ready-first sorting", async () => { + const now = Date.now(); + loadAccountsMock.mockResolvedValue({ + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "weekly-empty@example.com", + accountId: "acc_weekly_empty", + refreshToken: "refresh-weekly-empty", + accessToken: "access-weekly-empty", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "balanced@example.com", + accountId: "acc_balanced", + refreshToken: "refresh-balanced", + accessToken: "access-balanced", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }); + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings(), + ); + loadQuotaCacheMock.mockResolvedValue({ + byAccountId: {}, + byEmail: { + "weekly-empty@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 100, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "balanced@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }); + promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + const firstCallAccounts = promptLoginModeMock.mock.calls[0]?.[0] as Array<{ + email?: string; + quota5hLeftPercent?: number; + quota7dLeftPercent?: number; + }>; + expect(firstCallAccounts.map((account) => account.email)).toEqual([ + "balanced@example.com", + "weekly-empty@example.com", + ]); + expect(firstCallAccounts.map((account) => account.quota5hLeftPercent)).toEqual([ + 80, 100, + ]); + expect(firstCallAccounts.map((account) => account.quota7dLeftPercent)).toEqual([ + 80, 0, + ]); + }); + + it("treats missing quota windows as the lowest ready-first floor", async () => { + const now = Date.now(); + loadAccountsMock.mockResolvedValue({ + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "partial-window@example.com", + accountId: "acc_partial_window", + refreshToken: "refresh-partial-window", + accessToken: "access-partial-window", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "balanced-window@example.com", + accountId: "acc_balanced_window", + refreshToken: "refresh-balanced-window", + accessToken: "access-balanced-window", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }); + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings(), + ); + loadQuotaCacheMock.mockResolvedValue({ + byAccountId: {}, + byEmail: { + "partial-window@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 10, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: {}, + }, + "balanced-window@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }); + promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + const firstCallAccounts = promptLoginModeMock.mock.calls[0]?.[0] as Array<{ + email?: string; + quota5hLeftPercent?: number; + quota7dLeftPercent?: number; + quotaSummary?: string; + }>; + expect(firstCallAccounts.map((account) => account.email)).toEqual([ + "balanced-window@example.com", + "partial-window@example.com", + ]); + expect(firstCallAccounts.map((account) => account.quota5hLeftPercent)).toEqual([ + 80, 90, + ]); + expect(firstCallAccounts.map((account) => account.quota7dLeftPercent)).toEqual([ + 80, + undefined, + ]); + expect(firstCallAccounts[1]?.quotaSummary).toBe("5h 90%"); + }); + + it("treats accounts with no quota windows as the lowest ready-first floor", async () => { + const now = Date.now(); + loadAccountsMock.mockResolvedValue({ + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "missing-window@example.com", + accountId: "acc_missing_window", + refreshToken: "refresh-missing-window", + accessToken: "access-missing-window", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "full-window@example.com", + accountId: "acc_full_window", + refreshToken: "refresh-full-window", + accessToken: "access-full-window", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }); + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings(), + ); + loadQuotaCacheMock.mockResolvedValue({ + byAccountId: {}, + byEmail: { + "missing-window@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: {}, + secondary: {}, + }, + "full-window@example.com": { + updatedAt: now, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }); + promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + const firstCallAccounts = promptLoginModeMock.mock.calls[0]?.[0] as Array<{ + email?: string; + quota5hLeftPercent?: number; + quota7dLeftPercent?: number; + quotaSummary?: string; + }>; + expect(firstCallAccounts.map((account) => account.email)).toEqual([ + "full-window@example.com", + "missing-window@example.com", + ]); + expect(firstCallAccounts.map((account) => account.quota5hLeftPercent)).toEqual([ + 80, + undefined, + ]); + expect(firstCallAccounts.map((account) => account.quota7dLeftPercent)).toEqual([ + 80, + undefined, + ]); + }); + + it("re-sorts ready-first rows after async menu quota refresh changes which row is degraded", async () => { + const now = Date.now(); + let storageState = { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "becomes-degraded@example.com", + accountId: "acc_becomes_degraded", + refreshToken: "refresh-becomes-degraded", + accessToken: "access-becomes-degraded", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "becomes-healthy@example.com", + accountId: "acc_becomes_healthy", + refreshToken: "refresh-becomes-healthy", + accessToken: "access-becomes-healthy", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }; + loadAccountsMock.mockImplementation(async () => structuredClone(storageState)); + saveAccountsMock.mockImplementation(async (nextStorage) => { + storageState = structuredClone(nextStorage); + }); + + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings({ + menuAutoFetchLimits: true, + menuQuotaTtlMs: 1, + menuShowFetchStatus: true, + }), + ); + + let quotaCacheState = { + byAccountId: {}, + byEmail: { + "becomes-degraded@example.com": { + updatedAt: now - 10_000, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 10, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 10, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "becomes-healthy@example.com": { + updatedAt: now - 10_000, + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }; + loadQuotaCacheMock.mockImplementation(async () => structuredClone(quotaCacheState)); + + const releaseFirstRefresh = createDeferred(); + saveQuotaCacheMock.mockImplementation(async (nextCache) => { + quotaCacheState = structuredClone(nextCache); + }); + fetchCodexQuotaSnapshotMock + .mockImplementationOnce(async () => { + await releaseFirstRefresh.promise; + return { + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 3_000, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 4_000, + }, + }; + }) + .mockResolvedValueOnce({ + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 5_000, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 6_000, + }, + }); + + let promptCallCount = 0; + promptLoginModeMock + .mockImplementationOnce(async (accounts, options) => { + promptCallCount += 1; + expect(promptCallCount).toBe(1); + expect(accounts.map((account: { email?: string }) => account.email)).toEqual([ + "becomes-degraded@example.com", + "becomes-healthy@example.com", + ]); + expect( + accounts.map( + (account: { quotaRateLimited?: boolean }) => account.quotaRateLimited, + ), + ).toEqual([false, true]); + expect(typeof options?.statusMessage?.()).toBe("string"); + + releaseFirstRefresh.resolve(); + await vi.waitFor(() => { + expect(saveQuotaCacheMock).toHaveBeenCalledTimes(1); + }); + return { mode: "manage", deleteAccountIndex: 99 }; + }) + .mockImplementationOnce(async (accounts) => { + promptCallCount += 1; + expect(promptCallCount).toBe(2); + expect(accounts.map((account: { email?: string }) => account.email)).toEqual([ + "becomes-healthy@example.com", + "becomes-degraded@example.com", + ]); + expect( + accounts.map( + (account: { quotaRateLimited?: boolean }) => account.quotaRateLimited, + ), + ).toEqual([false, true]); + return { mode: "cancel" }; + }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + expect(promptLoginModeMock).toHaveBeenCalledTimes(2); + expect(promptCallCount).toBe(2); + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(2); + expect(saveQuotaCacheMock).toHaveBeenCalledTimes(1); + }); + it("prefers email-scoped quota cache entries for shared workspace accounts", async () => { const now = Date.now(); loadAccountsMock.mockResolvedValue({ From 11d52b3d6c3e8e1452463cfc56727a6c811f1b9a Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:33:31 +0800 Subject: [PATCH 05/18] fix: clear ready-first auto-refresh skip races --- lib/codex-manager.ts | 25 +- test/codex-manager-cli.test.ts | 417 ++++++++++++++++++++++++++++++--- 2 files changed, 400 insertions(+), 42 deletions(-) diff --git a/lib/codex-manager.ts b/lib/codex-manager.ts index f2cb284d8..3e9176401 100644 --- a/lib/codex-manager.ts +++ b/lib/codex-manager.ts @@ -2574,6 +2574,11 @@ async function runAuthLogin(args: string[]): Promise { let pendingMenuQuotaRefresh: Promise | null = null; let menuQuotaRefreshStatus: string | undefined; let skipNextMenuQuotaAutoRefresh = false; + let menuQuotaRefreshGeneration = 0; + const clearMenuQuotaAutoRefreshSkip = () => { + skipNextMenuQuotaAutoRefresh = false; + menuQuotaRefreshGeneration += 1; + }; loginFlow: while (true) { let existingStorage = await loadAccounts(); if (existingStorage && existingStorage.accounts.length > 0) { @@ -2610,6 +2615,7 @@ async function runAuthLogin(args: string[]): Promise { if (showFetchStatus) { menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [0/${staleCount}]`; } + const refreshGeneration = menuQuotaRefreshGeneration; pendingMenuQuotaRefresh = refreshQuotaCacheForMenu( currentStorage, quotaCache, @@ -2620,7 +2626,9 @@ async function runAuthLogin(args: string[]): Promise { }, ) .then(() => { - skipNextMenuQuotaAutoRefresh = true; + if (refreshGeneration === menuQuotaRefreshGeneration) { + skipNextMenuQuotaAutoRefresh = true; + } return undefined; }) .catch(() => undefined) @@ -2647,7 +2655,7 @@ async function runAuthLogin(args: string[]): Promise { return 0; } if (menuResult.mode === "check") { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await runActionPanel( "Quick Check", "Checking local session + live status", @@ -2659,7 +2667,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "deep-check") { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await runActionPanel( "Deep Check", "Refreshing and testing all accounts", @@ -2671,7 +2679,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "forecast") { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await runActionPanel( "Best Account", "Comparing accounts", @@ -2683,7 +2691,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "fix") { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await runActionPanel( "Auto-Fix", "Checking and fixing common issues", @@ -2695,12 +2703,12 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "settings") { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await configureUnifiedSettings(displaySettings); continue; } if (menuResult.mode === "verify-flagged") { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await runActionPanel( "Problem Account Check", "Checking problem accounts", @@ -2712,7 +2720,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "fresh" && menuResult.deleteAll) { - skipNextMenuQuotaAutoRefresh = false; + clearMenuQuotaAutoRefreshSkip(); await runActionPanel( "Reset Accounts", "Deleting all saved accounts", @@ -2727,6 +2735,7 @@ async function runAuthLogin(args: string[]): Promise { continue; } if (menuResult.mode === "manage") { + clearMenuQuotaAutoRefreshSkip(); const requiresInteractiveOAuth = typeof menuResult.refreshAccountIndex === "number"; if (requiresInteractiveOAuth) { diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index c91d78dc0..bf0dfe252 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -7320,38 +7320,44 @@ describe("codex manager cli commands", () => { saveQuotaCacheMock.mockImplementation(async (nextCache) => { quotaCacheState = structuredClone(nextCache); }); - fetchCodexQuotaSnapshotMock - .mockImplementationOnce(async () => { - await releaseFirstRefresh.promise; - return { - status: 429, - model: "gpt-5-codex", - primary: { - usedPercent: 0, - windowMinutes: 300, - resetAtMs: now + 3_000, - }, - secondary: { - usedPercent: 0, - windowMinutes: 10080, - resetAtMs: now + 4_000, - }, - }; - }) - .mockResolvedValueOnce({ - status: 200, - model: "gpt-5-codex", - primary: { - usedPercent: 20, - windowMinutes: 300, - resetAtMs: now + 5_000, - }, - secondary: { - usedPercent: 20, - windowMinutes: 10080, - resetAtMs: now + 6_000, - }, - }); + fetchCodexQuotaSnapshotMock.mockImplementation( + async ({ accountId }: { accountId?: string }) => { + if (accountId === "acc_becomes_degraded") { + await releaseFirstRefresh.promise; + return { + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 3_000, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 4_000, + }, + }; + } + if (accountId === "acc_becomes_healthy") { + return { + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 5_000, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 6_000, + }, + }; + } + throw new Error(`unexpected probe target: ${String(accountId)}`); + }, + ); let promptCallCount = 0; promptLoginModeMock @@ -7396,8 +7402,351 @@ describe("codex manager cli commands", () => { expect(exitCode).toBe(0); expect(promptLoginModeMock).toHaveBeenCalledTimes(2); expect(promptCallCount).toBe(2); - expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(2); - expect(saveQuotaCacheMock).toHaveBeenCalledTimes(1); + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(4); + expect(saveQuotaCacheMock).toHaveBeenCalledTimes(2); + }); + + it("does not let stale refresh completions re-enable the next menu auto-fetch skip", async () => { + const now = Date.now(); + let storageState = { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "becomes-degraded@example.com", + accountId: "acc_becomes_degraded", + refreshToken: "refresh-becomes-degraded", + accessToken: "access-becomes-degraded", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "becomes-healthy@example.com", + accountId: "acc_becomes_healthy", + refreshToken: "refresh-becomes-healthy", + accessToken: "access-becomes-healthy", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }; + loadAccountsMock.mockImplementation(async () => structuredClone(storageState)); + saveAccountsMock.mockImplementation(async (nextStorage) => { + storageState = structuredClone(nextStorage); + }); + + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings({ + menuAutoFetchLimits: true, + menuQuotaTtlMs: 0, + menuShowFetchStatus: true, + }), + ); + + let quotaCacheState = { + byAccountId: {}, + byEmail: { + "becomes-degraded@example.com": { + updatedAt: now - 10_000, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 10, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 10, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "becomes-healthy@example.com": { + updatedAt: now - 10_000, + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }; + loadQuotaCacheMock.mockImplementation(async () => structuredClone(quotaCacheState)); + + const releaseFirstRefresh = createDeferred(); + const releaseSecondRefresh = createDeferred(); + saveQuotaCacheMock.mockImplementation(async (nextCache) => { + quotaCacheState = structuredClone(nextCache); + }); + + let degradedProbeCount = 0; + let healthyProbeCount = 0; + fetchCodexQuotaSnapshotMock.mockImplementation( + async ({ accountId }: { accountId?: string }) => { + if (accountId === "acc_becomes_degraded") { + degradedProbeCount += 1; + if (degradedProbeCount === 1) { + await releaseFirstRefresh.promise; + } else if (degradedProbeCount === 2) { + await releaseSecondRefresh.promise; + } + return { + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 3_000 + degradedProbeCount, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 4_000 + degradedProbeCount, + }, + }; + } + if (accountId === "acc_becomes_healthy") { + healthyProbeCount += 1; + return { + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 5_000 + healthyProbeCount, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 6_000 + healthyProbeCount, + }, + }; + } + throw new Error(`unexpected probe target: ${String(accountId)}`); + }, + ); + + let promptCallCount = 0; + promptLoginModeMock + .mockImplementationOnce(async (accounts) => { + promptCallCount += 1; + expect(promptCallCount).toBe(1); + expect(accounts.map((account: { email?: string }) => account.email)).toEqual([ + "becomes-degraded@example.com", + "becomes-healthy@example.com", + ]); + + queueMicrotask(() => { + releaseFirstRefresh.resolve(); + }); + return { mode: "manage", deleteAccountIndex: 99 }; + }) + .mockImplementationOnce(async (accounts, options) => { + promptCallCount += 1; + expect(promptCallCount).toBe(2); + expect(accounts.map((account: { email?: string }) => account.email)).toEqual([ + "becomes-healthy@example.com", + "becomes-degraded@example.com", + ]); + expect(typeof options?.statusMessage?.()).toBe("string"); + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(3); + + releaseSecondRefresh.resolve(); + await vi.waitFor(() => { + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(4); + }); + return { mode: "cancel" }; + }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + expect(promptLoginModeMock).toHaveBeenCalledTimes(2); + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(4); + expect(saveQuotaCacheMock).toHaveBeenCalledTimes(2); + }); + + it("does not get stuck skipping menu auto-fetch when quota cache saves fail with EBUSY", async () => { + const now = Date.now(); + let storageState = { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "becomes-degraded@example.com", + accountId: "acc_becomes_degraded", + refreshToken: "refresh-becomes-degraded", + accessToken: "access-becomes-degraded", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "becomes-healthy@example.com", + accountId: "acc_becomes_healthy", + refreshToken: "refresh-becomes-healthy", + accessToken: "access-becomes-healthy", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }; + loadAccountsMock.mockImplementation(async () => structuredClone(storageState)); + saveAccountsMock.mockImplementation(async (nextStorage) => { + storageState = structuredClone(nextStorage); + }); + + loadDashboardDisplaySettingsMock.mockResolvedValue( + createReadyFirstMenuSettings({ + menuAutoFetchLimits: true, + menuQuotaTtlMs: 0, + menuShowFetchStatus: true, + }), + ); + + let quotaCacheState = { + byAccountId: {}, + byEmail: { + "becomes-degraded@example.com": { + updatedAt: now - 10_000, + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 10, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 10, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + "becomes-healthy@example.com": { + updatedAt: now - 10_000, + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 1_000, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 2_000, + }, + }, + }, + }; + loadQuotaCacheMock.mockImplementation(async () => structuredClone(quotaCacheState)); + + const releaseFirstRefresh = createDeferred(); + const releaseSecondRefresh = createDeferred(); + let saveAttemptCount = 0; + saveQuotaCacheMock.mockImplementation(async (nextCache) => { + saveAttemptCount += 1; + if (saveAttemptCount === 1) { + throw makeErrnoError("quota cache busy", "EBUSY"); + } + quotaCacheState = structuredClone(nextCache); + }); + + let degradedProbeCount = 0; + let healthyProbeCount = 0; + fetchCodexQuotaSnapshotMock.mockImplementation( + async ({ accountId }: { accountId?: string }) => { + if (accountId === "acc_becomes_degraded") { + degradedProbeCount += 1; + if (degradedProbeCount === 1) { + await releaseFirstRefresh.promise; + } else if (degradedProbeCount === 2) { + await releaseSecondRefresh.promise; + } + return { + status: 429, + model: "gpt-5-codex", + primary: { + usedPercent: 0, + windowMinutes: 300, + resetAtMs: now + 3_000 + degradedProbeCount, + }, + secondary: { + usedPercent: 0, + windowMinutes: 10080, + resetAtMs: now + 4_000 + degradedProbeCount, + }, + }; + } + if (accountId === "acc_becomes_healthy") { + healthyProbeCount += 1; + return { + status: 200, + model: "gpt-5-codex", + primary: { + usedPercent: 20, + windowMinutes: 300, + resetAtMs: now + 5_000 + healthyProbeCount, + }, + secondary: { + usedPercent: 20, + windowMinutes: 10080, + resetAtMs: now + 6_000 + healthyProbeCount, + }, + }; + } + throw new Error(`unexpected probe target: ${String(accountId)}`); + }, + ); + + let promptCallCount = 0; + promptLoginModeMock + .mockImplementationOnce(async () => { + promptCallCount += 1; + expect(promptCallCount).toBe(1); + + queueMicrotask(() => { + releaseFirstRefresh.resolve(); + }); + return { mode: "manage", deleteAccountIndex: 99 }; + }) + .mockImplementationOnce(async (_accounts, options) => { + promptCallCount += 1; + expect(promptCallCount).toBe(2); + expect(typeof options?.statusMessage?.()).toBe("string"); + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(3); + + releaseSecondRefresh.resolve(); + await vi.waitFor(() => { + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(4); + }); + return { mode: "cancel" }; + }); + + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + expect(promptLoginModeMock).toHaveBeenCalledTimes(2); + expect(fetchCodexQuotaSnapshotMock).toHaveBeenCalledTimes(4); + expect(saveQuotaCacheMock).toHaveBeenCalledTimes(2); }); it("prefers email-scoped quota cache entries for shared workspace accounts", async () => { From 8151c08c25348d1ba5ad8b056ef0ccd8d7cc7c4e Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:43:46 +0800 Subject: [PATCH 06/18] fix codex wrapper compatibility handling --- scripts/codex-bin-resolver.js | 97 ++++++ scripts/codex.js | 492 ++++++++++++++++++++++---- test/codex-bin-wrapper.test.ts | 606 ++++++++++++++++++++++++++++++++- 3 files changed, 1125 insertions(+), 70 deletions(-) create mode 100644 scripts/codex-bin-resolver.js diff --git a/scripts/codex-bin-resolver.js b/scripts/codex-bin-resolver.js new file mode 100644 index 000000000..31f7e7bd8 --- /dev/null +++ b/scripts/codex-bin-resolver.js @@ -0,0 +1,97 @@ +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +function defaultResolvePackageBin(moduleUrl) { + try { + const require = createRequire(moduleUrl); + return require.resolve("@openai/codex/bin/codex.js"); + } catch { + return null; + } +} + +function resolveWindowsCmdPath(env) { + const comSpec = (env.ComSpec ?? env.COMSPEC ?? "").trim(); + if (comSpec.length > 0) return comSpec; + + const systemRoot = (env.SystemRoot ?? env.SYSTEMROOT ?? "").trim(); + if (systemRoot.length > 0) { + return `${systemRoot.replace(/[\\/]+$/, "")}\\System32\\cmd.exe`; + } + + return "cmd.exe"; +} + +export function resolveRealCodexBin(options = {}) { + const { + env = process.env, + argv = process.argv, + platform = process.platform, + moduleUrl = import.meta.url, + existsSyncImpl = existsSync, + spawnSyncImpl = spawnSync, + resolvePackageBin = defaultResolvePackageBin, + } = options; + + const override = (env.CODEX_MULTI_AUTH_REAL_CODEX_BIN ?? "").trim(); + if (override.length > 0) { + if (existsSyncImpl(override)) return override; + return null; + } + + const resolved = resolvePackageBin(moduleUrl); + if (typeof resolved === "string" && resolved.length > 0 && existsSyncImpl(resolved)) { + return resolved; + } + + const searchRoots = []; + const scriptDir = dirname(fileURLToPath(moduleUrl)); + searchRoots.push(join(scriptDir, "..", "..")); + + const invokedScript = argv[1]; + if (typeof invokedScript === "string" && invokedScript.length > 0) { + searchRoots.push(join(dirname(invokedScript), "..", "..")); + } + + const npmPrefix = (env.npm_config_prefix ?? env.PREFIX ?? "").trim(); + if (npmPrefix.length > 0) { + searchRoots.push(join(npmPrefix, "node_modules")); + searchRoots.push(join(npmPrefix, "lib", "node_modules")); + } + + for (const root of searchRoots) { + const candidate = join(root, "@openai", "codex", "bin", "codex.js"); + if (existsSyncImpl(candidate)) return candidate; + } + + try { + const rootResult = + platform === "win32" + ? spawnSyncImpl(resolveWindowsCmdPath(env), ["/d", "/s", "/c", "npm root -g"], { + encoding: "utf8", + env, + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }) + : spawnSyncImpl("npm", ["root", "-g"], { + encoding: "utf8", + env, + stdio: ["ignore", "pipe", "ignore"], + }); + if (rootResult.status === 0) { + const globalRoot = rootResult.stdout.trim(); + if (globalRoot.length > 0) { + const globalBin = join(globalRoot, "@openai", "codex", "bin", "codex.js"); + if (existsSyncImpl(globalBin)) return globalBin; + } + } + } catch { + // Ignore and fall through to null. + } + + return null; +} diff --git a/scripts/codex.js b/scripts/codex.js index 148f0b45c..a85a31446 100755 --- a/scripts/codex.js +++ b/scripts/codex.js @@ -1,13 +1,55 @@ #!/usr/bin/env node -import { spawn, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { spawn } from "node:child_process"; +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { createRequire } from "node:module"; +import { homedir, tmpdir } from "node:os"; import { basename, delimiter, dirname, join, resolve as resolvePath } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; +import { resolveRealCodexBin as resolveRealCodexBinFromEnvironment } from "./codex-bin-resolver.js"; import { normalizeAuthAlias, shouldHandleMultiAuthAuth } from "./codex-routing.js"; +const RETRYABLE_SHADOW_HOME_CLEANUP_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]); +const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120]; + +function isRetryableShadowHomeCleanupError(error) { + const code = error && typeof error === "object" && "code" in error ? error.code : undefined; + return typeof code === "string" && RETRYABLE_SHADOW_HOME_CLEANUP_CODES.has(code); +} + +function sleepSync(ms) { + if (!Number.isFinite(ms) || ms <= 0) { + return; + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); +} + +function removeDirectoryWithRetry(targetPath) { + for (let attempt = 0; attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length; attempt += 1) { + try { + rmSync(targetPath, { recursive: true, force: true }); + return; + } catch (error) { + if ( + !isRetryableShadowHomeCleanupError(error) || + attempt === SHADOW_HOME_CLEANUP_BACKOFF_MS.length + ) { + throw error; + } + sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]); + } + } +} + function hydrateCliVersionEnv() { try { const require = createRequire(import.meta.url); @@ -74,73 +116,37 @@ function resolveRealCodexBin() { return null; } - try { - const require = createRequire(import.meta.url); - const resolved = require.resolve("@openai/codex/bin/codex.js"); - if (existsSync(resolved)) return resolved; - } catch { - // Fall through to sibling lookup. - } - - const searchRoots = []; - const scriptDir = dirname(fileURLToPath(import.meta.url)); - searchRoots.push(join(scriptDir, "..", "..")); - - const invokedScript = process.argv[1]; - if (typeof invokedScript === "string" && invokedScript.length > 0) { - searchRoots.push(join(dirname(invokedScript), "..", "..")); - } - - const npmPrefix = (process.env.npm_config_prefix ?? process.env.PREFIX ?? "").trim(); - if (npmPrefix.length > 0) { - searchRoots.push(join(npmPrefix, "node_modules")); - searchRoots.push(join(npmPrefix, "lib", "node_modules")); - } - - for (const root of searchRoots) { - const candidate = join(root, "@openai", "codex", "bin", "codex.js"); - if (existsSync(candidate)) return candidate; - } - - const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; - try { - const rootResult = spawnSync(npmCmd, ["root", "-g"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - if (rootResult.status === 0) { - const globalRoot = rootResult.stdout.trim(); - if (globalRoot.length > 0) { - const globalBin = join(globalRoot, "@openai", "codex", "bin", "codex.js"); - if (existsSync(globalBin)) return globalBin; - } - } - } catch { - // Ignore and fall through to null. - } - - return null; + return resolveRealCodexBinFromEnvironment({ moduleUrl: import.meta.url }); } -function forwardToRealCodex(codexBin, args) { +function forwardToRealCodex(codexBin, args, env = process.env, cleanup) { return new Promise((resolve) => { + const finalize = (exitCode) => { + try { + cleanup?.(); + } catch { + // Best-effort cleanup only. + } + resolve(exitCode); + }; + const child = spawn(process.execPath, [codexBin, ...args], { stdio: "inherit", - env: process.env, + env, }); child.once("error", (error) => { console.error(`Failed to launch real Codex CLI: ${String(error)}`); - resolve(1); + finalize(1); }); child.once("exit", (code, signal) => { if (signal) { const signalNumber = signal === "SIGINT" ? 130 : 1; - resolve(signalNumber); + finalize(signalNumber); return; } - resolve(typeof code === "number" ? code : 1); + finalize(typeof code === "number" ? code : 1); }); }); } @@ -168,13 +174,379 @@ function hasCliAuthCredentialsStoreOverride(args) { return false; } +// IMPORTANT: Keep this mapping in sync with +// `lib/request/helpers/model-map.ts` `MODEL_PROFILES`. +// This wrapper runs before the TypeScript build, so it cannot import that source. +const SUPPORTED_REASONING_EFFORTS_BY_MODEL = { + "gpt-5-codex": ["low", "medium", "high", "xhigh"], + "gpt-5.1-codex-max": ["medium", "high", "xhigh"], + "gpt-5.1-codex-mini": ["medium", "high"], + "gpt-5.4": ["none", "low", "medium", "high", "xhigh"], + "gpt-5.4-pro": ["medium", "high", "xhigh"], + "gpt-5.2-pro": ["medium", "high", "xhigh"], + "gpt-5-pro": ["high"], + "gpt-5.2": ["none", "low", "medium", "high", "xhigh"], + "gpt-5.1": ["none", "low", "medium", "high"], + "gpt-5": ["minimal", "low", "medium", "high"], + "gpt-5-mini": ["medium"], + "gpt-5-nano": ["medium"], +}; + +const REASONING_FALLBACKS = { + none: ["none", "low", "minimal", "medium", "high", "xhigh"], + minimal: ["minimal", "low", "none", "medium", "high", "xhigh"], + low: ["low", "minimal", "none", "medium", "high", "xhigh"], + medium: ["medium", "low", "high", "minimal", "none", "xhigh"], + high: ["high", "medium", "xhigh", "low", "minimal", "none"], + xhigh: ["xhigh", "high", "medium", "low", "minimal", "none"], +}; + +const KNOWN_REASONING_EFFORTS = new Set(Object.keys(REASONING_FALLBACKS)); + +function stripProviderPrefix(model) { + return typeof model === "string" && model.includes("/") + ? model.split("/").pop() ?? model + : model; +} + +function normalizeRequestedModel(model) { + const stripped = stripProviderPrefix(model ?? ""); + const normalized = stripped.trim().toLowerCase(); + if (normalized.length === 0) return ""; + + if ( + normalized.includes("gpt-5.1-codex-max") || + normalized.includes("gpt 5.1 codex max") || + normalized.includes("codex-max") + ) { + return "gpt-5.1-codex-max"; + } + if ( + normalized.includes("gpt-5.1-codex-mini") || + normalized.includes("gpt 5.1 codex mini") || + normalized.includes("gpt-5-codex-mini") || + normalized.includes("gpt 5 codex mini") || + normalized.includes("codex-mini-latest") + ) { + return "gpt-5.1-codex-mini"; + } + if ( + normalized.includes("gpt-5.3-codex-spark") || + normalized.includes("gpt 5.3 codex spark") || + normalized.includes("gpt-5.3-codex") || + normalized.includes("gpt 5.3 codex") || + normalized.includes("gpt-5.2-codex") || + normalized.includes("gpt 5.2 codex") || + normalized.includes("gpt-5.1-codex") || + normalized.includes("gpt 5.1 codex") || + normalized.includes("gpt-5-codex") || + normalized.includes("gpt 5 codex") || + normalized === "codex" + ) { + return "gpt-5-codex"; + } + if (normalized.includes("gpt-5.4-pro") || normalized.includes("gpt 5.4 pro")) { + return "gpt-5.4-pro"; + } + if (normalized.includes("gpt-5.2-pro") || normalized.includes("gpt 5.2 pro")) { + return "gpt-5.2-pro"; + } + if (normalized.includes("gpt-5-pro") || normalized.includes("gpt 5 pro")) { + return "gpt-5-pro"; + } + if ( + normalized.includes("gpt-5.4-mini") || + normalized.includes("gpt 5.4 mini") || + normalized.includes("gpt-5-mini") || + normalized.includes("gpt 5 mini") + ) { + return "gpt-5-mini"; + } + if ( + normalized.includes("gpt-5.4-nano") || + normalized.includes("gpt 5.4 nano") || + normalized.includes("gpt-5-nano") || + normalized.includes("gpt 5 nano") + ) { + return "gpt-5-nano"; + } + if (normalized.includes("gpt-5.4") || normalized.includes("gpt 5.4")) { + return "gpt-5.4"; + } + if (normalized.includes("gpt-5.2") || normalized.includes("gpt 5.2")) { + return "gpt-5.2"; + } + if ( + normalized.includes("gpt-5.1-chat-latest") || + normalized.includes("gpt-5.1") || + normalized.includes("gpt 5.1") + ) { + return "gpt-5.1"; + } + if ( + normalized === "gpt-5-chat-latest" || + normalized === "gpt-5" || + normalized.includes("gpt 5") + ) { + return "gpt-5"; + } + return ""; +} + +function coerceReasoningEffortForModel(model, effort) { + if (typeof effort !== "string") return effort; + const normalizedEffort = effort.trim().toLowerCase(); + if (!KNOWN_REASONING_EFFORTS.has(normalizedEffort)) { + return effort; + } + + const normalizedModel = normalizeRequestedModel(model); + const supportedEfforts = + SUPPORTED_REASONING_EFFORTS_BY_MODEL[normalizedModel] ?? null; + if (!supportedEfforts || supportedEfforts.includes(normalizedEffort)) { + return normalizedEffort; + } + + const fallbackOrder = REASONING_FALLBACKS[normalizedEffort] ?? [normalizedEffort]; + for (const candidate of fallbackOrder) { + if (supportedEfforts.includes(candidate)) { + return candidate; + } + } + + return normalizedEffort; +} + +function extractRequestedModel(args) { + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === "--model") { + const next = args[i + 1]; + if (typeof next === "string" && next.trim().length > 0) { + return next.trim(); + } + continue; + } + if (typeof arg === "string" && arg.startsWith("--model=")) { + const value = arg.slice("--model=".length).trim(); + if (value.length > 0) { + return value; + } + } + } + return null; +} + +function parseConfigAssignment(value) { + if (typeof value !== "string") return null; + const separatorIndex = value.indexOf("="); + if (separatorIndex < 0) return null; + return { + key: value.slice(0, separatorIndex).trim(), + value: value.slice(separatorIndex + 1).trim(), + }; +} + +function parseQuotedValue(value) { + if (typeof value !== "string") { + return { quote: '"', inner: "" }; + } + const trimmed = value.trim(); + const first = trimmed[0]; + const last = trimmed.at(-1); + if ((first === '"' || first === "'") && last === first) { + return { + quote: first, + inner: trimmed.slice(1, -1), + }; + } + return { quote: '"', inner: trimmed }; +} + +function rewriteReasoningConfigAssignment(assignment, requestedModel) { + const parsed = parseConfigAssignment(assignment); + if (!parsed || parsed.key !== "model_reasoning_effort") { + return assignment; + } + + const { quote, inner } = parseQuotedValue(parsed.value); + const coercedEffort = coerceReasoningEffortForModel(requestedModel, inner); + if (coercedEffort === inner) { + return assignment; + } + + return `${parsed.key}=${quote}${coercedEffort}${quote}`; +} + +function rewriteReasoningConfigArgs(rawArgs) { + const requestedModel = extractRequestedModel(rawArgs); + if (!requestedModel) { + return { + args: [...rawArgs], + requestedModel: null, + }; + } + + const nextArgs = [...rawArgs]; + for (let i = 0; i < nextArgs.length; i += 1) { + const arg = nextArgs[i]; + if ((arg === "-c" || arg === "--config") && typeof nextArgs[i + 1] === "string") { + nextArgs[i + 1] = rewriteReasoningConfigAssignment( + nextArgs[i + 1], + requestedModel, + ); + i += 1; + continue; + } + if (typeof arg === "string" && arg.startsWith("--config=")) { + const assignment = arg.slice("--config=".length); + nextArgs[i] = `--config=${rewriteReasoningConfigAssignment( + assignment, + requestedModel, + )}`; + } + } + + return { + args: nextArgs, + requestedModel, + }; +} + +function resolveCodexHomeDir(env = process.env) { + const override = (env.CODEX_HOME ?? "").trim(); + if (override.length > 0) return override; + if (process.platform === "win32") { + const homeDir = resolveWindowsUserHomeDir(); + if (homeDir) { + return join(homeDir, ".codex"); + } + } + return join(env.HOME ?? homedir(), ".codex"); +} + +function ensureTrailingNewline(value) { + return value.endsWith("\n") ? value : `${value}\n`; +} + +function rewriteConfigTomlReasoningEffort(rawConfig, requestedModel) { + const lineEnding = rawConfig.includes("\r\n") ? "\r\n" : "\n"; + let changed = false; + const nextLines = rawConfig.split(/\r?\n/).map((line) => { + const quotedMatch = line.match( + /^(\s*model_reasoning_effort\s*=\s*)(["'])([^"']+)(\2.*)$/, + ); + const bareMatch = quotedMatch + ? null + : line.match( + /^(\s*model_reasoning_effort\s*=\s*)([^\s#]+)(\s*(?:#.*)?)$/, + ); + if (!quotedMatch && !bareMatch) return line; + + const prefix = quotedMatch?.[1] ?? bareMatch?.[1] ?? ""; + const openingQuote = quotedMatch?.[2] ?? ""; + const currentEffort = quotedMatch?.[3] ?? bareMatch?.[2] ?? ""; + const suffix = quotedMatch?.[4] ?? bareMatch?.[3] ?? ""; + const coercedEffort = coerceReasoningEffortForModel( + requestedModel, + currentEffort, + ); + if (coercedEffort === currentEffort) { + return line; + } + + changed = true; + return quotedMatch + ? `${prefix}${openingQuote}${coercedEffort}${suffix}` + : `${prefix}${coercedEffort}${suffix}`; + }); + + if (!changed) { + return rawConfig; + } + + return ensureTrailingNewline(nextLines.join(lineEnding)); +} + +function resolveOriginalMultiAuthDir(env) { + const explicit = (env.CODEX_MULTI_AUTH_DIR ?? "").trim(); + if (explicit.length > 0) { + return explicit; + } + return undefined; +} + +function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { + const { args: nextArgs, requestedModel } = rewriteReasoningConfigArgs(rawArgs); + if (!requestedModel) { + return { args: nextArgs, env: baseEnv, cleanup: undefined }; + } + + const xhighCompatEffort = coerceReasoningEffortForModel(requestedModel, "xhigh"); + if (xhighCompatEffort === "xhigh") { + return { args: nextArgs, env: baseEnv, cleanup: undefined }; + } + + const originalCodexHome = resolveCodexHomeDir(baseEnv); + const configPath = join(originalCodexHome, "config.toml"); + if (!existsSync(configPath)) { + return { args: nextArgs, env: baseEnv, cleanup: undefined }; + } + + const rawConfig = readFileSync(configPath, "utf8"); + const compatConfig = rewriteConfigTomlReasoningEffort( + rawConfig, + requestedModel, + ); + if (compatConfig === rawConfig) { + return { args: nextArgs, env: baseEnv, cleanup: undefined }; + } + + const shadowCodexHome = mkdtempSync(join(tmpdir(), "codex-multi-auth-home-")); + const cleanup = () => { + try { + removeDirectoryWithRetry(shadowCodexHome); + } catch { + // Best-effort cleanup only. + } + }; + try { + writeFileSync(join(shadowCodexHome, "config.toml"), compatConfig, "utf8"); + for (const name of ["auth.json", "accounts.json", ".codex-global-state.json"]) { + const sourcePath = join(originalCodexHome, name); + if (existsSync(sourcePath)) { + copyFileSync(sourcePath, join(shadowCodexHome, name)); + } + } + } catch (error) { + cleanup(); + throw error; + } + + const forwardedEnv = { + ...baseEnv, + CODEX_HOME: shadowCodexHome, + }; + const originalMultiAuthDir = resolveOriginalMultiAuthDir(baseEnv); + if (originalMultiAuthDir) { + forwardedEnv.CODEX_MULTI_AUTH_DIR = originalMultiAuthDir; + } + + return { + args: nextArgs, + env: forwardedEnv, + cleanup, + }; +} + function buildForwardArgs(rawArgs) { + const { args: compatibilityArgs } = rewriteReasoningConfigArgs(rawArgs); const forceFileAuthStore = (process.env.CODEX_MULTI_AUTH_FORCE_FILE_AUTH_STORE ?? "1").trim() !== "0"; - if (!forceFileAuthStore) return [...rawArgs]; - if (hasCliAuthCredentialsStoreOverride(rawArgs)) return [...rawArgs]; + if (!forceFileAuthStore) return compatibilityArgs; + if (hasCliAuthCredentialsStoreOverride(compatibilityArgs)) return compatibilityArgs; return [ - ...rawArgs, + ...compatibilityArgs, "-c", 'cli_auth_credentials_store="file"', ]; @@ -536,7 +908,13 @@ async function main() { await autoSyncManagerActiveSelectionIfEnabled(); const forwardArgs = buildForwardArgs(rawArgs); - return forwardToRealCodex(realCodexBin, forwardArgs); + const compatibility = createCompatibilityCodexHome(forwardArgs); + return forwardToRealCodex( + realCodexBin, + compatibility.args, + compatibility.env, + compatibility.cleanup, + ); } const exitCode = await main(); diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index a1c30e60f..0bf42cd11 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -3,6 +3,7 @@ import { copyFileSync, mkdirSync, mkdtempSync, + readdirSync, readFileSync, rmSync, writeFileSync, @@ -10,9 +11,10 @@ import { import { tmpdir } from "node:os"; import { delimiter, dirname, join } from "node:path"; import process from "node:process"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { sleep } from "../lib/utils.js"; +import { resolveRealCodexBin } from "../scripts/codex-bin-resolver.js"; const createdDirs: string[] = []; const testFileDir = dirname(fileURLToPath(import.meta.url)); @@ -57,6 +59,10 @@ function createWrapperFixture(): string { join(repoRootDir, "scripts", "codex-routing.js"), join(scriptDir, "codex-routing.js"), ); + copyFileSync( + join(repoRootDir, "scripts", "codex-bin-resolver.js"), + join(scriptDir, "codex-bin-resolver.js"), + ); return fixtureRoot; } @@ -80,6 +86,75 @@ function createCustomFakeCodexBin(rootDir: string, lines: string[]): string { return fakeBin; } +function injectShadowCleanupBusyFailures( + fixtureRoot: string, + failuresBeforeSuccess = 2, +): void { + const wrapperPath = join(fixtureRoot, "scripts", "codex.js"); + const originalSource = readFileSync(wrapperPath, "utf8"); + const instrumentedSource = originalSource + .replace( + 'const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120];', + [ + 'const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120];', + `globalThis.__cleanupBusyFailuresRemaining = ${failuresBeforeSuccess};`, + ].join("\n"), + ) + .replace( + "rmSync(targetPath, { recursive: true, force: true });", + [ + "if (globalThis.__cleanupBusyFailuresRemaining > 0) {", + "\tglobalThis.__cleanupBusyFailuresRemaining -= 1;", + '\tconst error = new Error("simulated busy cleanup");', + '\terror.code = "EBUSY";', + "\tthrow error;", + "}", + "rmSync(targetPath, { recursive: true, force: true });", + ].join("\n"), + ); + writeFileSync(wrapperPath, instrumentedSource, "utf8"); +} + +function createFakeGlobalCodexInstall(rootDir: string): string { + const fakeBin = join(rootDir, "@openai", "codex", "bin", "codex.js"); + mkdirSync(dirname(fakeBin), { recursive: true }); + writeFileSync( + fakeBin, + [ + "#!/usr/bin/env node", + 'console.log(`FORWARDED:${process.argv.slice(2).join(" ")}`);', + "process.exit(0);", + ].join("\n"), + "utf8", + ); + return fakeBin; +} + +function createSpawnSyncSuccess(stdout: string): SpawnSyncReturns { + return { + output: ["", stdout, ""], + pid: 1, + signal: null, + status: 0, + stderr: "", + stdout, + }; +} + +function buildWrapperEnv(extraEnv: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { + ...process.env, + CODEX_MULTI_AUTH_FORCE_FILE_AUTH_STORE: "1", + ...extraEnv, + }; + for (const [key, value] of Object.entries(env)) { + if (value === undefined) { + delete env[key]; + } + } + return env; +} + function runWrapper( fixtureRoot: string, args: string[], @@ -90,10 +165,7 @@ function runWrapper( [join(fixtureRoot, "scripts", "codex.js"), ...args], { encoding: "utf8", - env: { - ...process.env, - ...extraEnv, - }, + env: buildWrapperEnv(extraEnv), }, ); } @@ -105,10 +177,7 @@ function runWrapperScript( ): SpawnSyncReturns { return spawnSync(process.execPath, [scriptPath, ...args], { encoding: "utf8", - env: { - ...process.env, - ...extraEnv, - }, + env: buildWrapperEnv(extraEnv), }); } @@ -129,10 +198,7 @@ function runWrapperAsync( process.execPath, [join(fixtureRoot, "scripts", "codex.js"), ...args], { - env: { - ...process.env, - ...extraEnv, - }, + env: buildWrapperEnv(extraEnv), stdio: ["ignore", "pipe", "pipe"], }, ); @@ -269,6 +335,232 @@ describe("codex bin wrapper", () => { expect(combinedOutput(result)).toContain("EPERM: locked auth store"); }); + it("creates a compatibility CODEX_HOME shadow when the requested model cannot accept xhigh defaults", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ + "#!/usr/bin/env node", + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'console.log(`FORWARDED:${process.argv.slice(2).join(" ")}`);', + 'console.log(`CODEX_HOME:${process.env.CODEX_HOME ?? ""}`);', + 'console.log(`CODEX_MULTI_AUTH_DIR_JSON:${JSON.stringify(process.env.CODEX_MULTI_AUTH_DIR ?? null)}`);', + 'const configPath = path.join(process.env.CODEX_HOME ?? "", "config.toml");', + 'const authPath = path.join(process.env.CODEX_HOME ?? "", "auth.json");', + 'console.log(`AUTH_EXISTS:${fs.existsSync(authPath)}`);', + 'if (fs.existsSync(authPath)) console.log(`AUTH_JSON:${fs.readFileSync(authPath, "utf8").trim()}`);', + 'console.log("CONFIG_START");', + 'console.log(fs.readFileSync(configPath, "utf8").trim());', + 'console.log("CONFIG_END");', + "process.exit(0);", + ]); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync( + join(originalHome, "config.toml"), + [ + 'model_reasoning_effort = "xhigh"', + 'profile = "legacy-full-access"', + "", + '[profiles."legacy-full-access"]', + 'model_reasoning_effort = "xhigh"', + "", + ].join("\n"), + "utf8", + ); + + const result = runWrapper(fixtureRoot, ["exec", "status", "--model", "gpt-5.1"], { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + CODEX_MULTI_AUTH_DIR: undefined, + }); + + expect(result.status).toBe(0); + const output = combinedOutput(result); + expect(output).toContain('FORWARDED:exec status --model gpt-5.1 -c cli_auth_credentials_store="file"'); + expect(output).not.toContain(`CODEX_HOME:${originalHome}`); + expect(output).toContain("CODEX_MULTI_AUTH_DIR_JSON:null"); + expect(output).toContain("AUTH_EXISTS:true"); + expect(output).toContain("AUTH_JSON:{}"); + expect(output).toContain('model_reasoning_effort = "high"'); + expect(output).not.toContain('model_reasoning_effort = "xhigh"'); + }); + + it("cleans up compatibility shadow homes when staging fails", () => { + const fixtureRoot = createWrapperFixture(); + injectShadowCleanupBusyFailures(fixtureRoot); + const fakeBin = createFakeCodexBin(fixtureRoot); + const originalHome = join(fixtureRoot, "codex-home"); + const controlledTmp = join(fixtureRoot, "tmp"); + mkdirSync(originalHome, { recursive: true }); + mkdirSync(controlledTmp, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + mkdirSync(join(originalHome, "accounts.json"), { recursive: true }); + writeFileSync( + join(originalHome, "config.toml"), + 'model_reasoning_effort = "xhigh"\n', + "utf8", + ); + + const result = runWrapper( + fixtureRoot, + ["exec", "status", "--model", "gpt-5.1"], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + TMP: controlledTmp, + TEMP: controlledTmp, + }, + ); + + expect(result.status).toBe(1); + expect( + readdirSync(controlledTmp).filter((entry) => + entry.startsWith("codex-multi-auth-home-"), + ), + ).toEqual([]); + }); + + it("rewrites unquoted config reasoning effort values for mini compatibility models", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ + "#!/usr/bin/env node", + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'console.log(`FORWARDED:${process.argv.slice(2).join(" ")}`);', + 'const configPath = path.join(process.env.CODEX_HOME ?? "", "config.toml");', + 'console.log("CONFIG_START");', + 'console.log(fs.readFileSync(configPath, "utf8").trim());', + 'console.log("CONFIG_END");', + "process.exit(0);", + ]); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync( + join(originalHome, "config.toml"), + [ + "model_reasoning_effort = xhigh", + 'profile = "legacy-full-access"', + "", + '[profiles."legacy-full-access"]', + "model_reasoning_effort = xhigh # keep comment", + "", + ].join("\n"), + "utf8", + ); + + const result = runWrapper( + fixtureRoot, + ["exec", "status", "--model", "gpt-5.1-codex-mini"], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + }, + ); + + expect(result.status).toBe(0); + const output = combinedOutput(result); + expect(output).toContain( + 'FORWARDED:exec status --model gpt-5.1-codex-mini -c cli_auth_credentials_store="file"', + ); + expect(output).toContain("model_reasoning_effort = high"); + expect(output).toContain("model_reasoning_effort = high # keep comment"); + expect(output).not.toContain("model_reasoning_effort = xhigh"); + }); + + it("downgrades explicit unsupported reasoning overrides before forwarding", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createFakeCodexBin(fixtureRoot); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync(join(originalHome, "config.toml"), "", "utf8"); + + const result = runWrapper( + fixtureRoot, + [ + "exec", + "status", + "--model", + "gpt-5.1", + "-c", + 'model_reasoning_effort="xhigh"', + ], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + 'FORWARDED:exec status --model gpt-5.1 -c model_reasoning_effort="high" -c cli_auth_credentials_store="file"', + ); + expect(result.stdout).not.toContain('model_reasoning_effort="xhigh"'); + }); + + it("downgrades explicit unsupported reasoning overrides for codex mini variants", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createFakeCodexBin(fixtureRoot); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync(join(originalHome, "config.toml"), "", "utf8"); + + const result = runWrapper( + fixtureRoot, + [ + "exec", + "status", + "--model", + "gpt-5.1-codex-mini", + "-c", + 'model_reasoning_effort="xhigh"', + ], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + 'FORWARDED:exec status --model gpt-5.1-codex-mini -c model_reasoning_effort="high" -c cli_auth_credentials_store="file"', + ); + expect(result.stdout).not.toContain('model_reasoning_effort="xhigh"'); + }); + + it("preserves explicit xhigh overrides for models that support them", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createFakeCodexBin(fixtureRoot); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync(join(originalHome, "config.toml"), "", "utf8"); + + const result = runWrapper( + fixtureRoot, + [ + "exec", + "status", + "--model", + "gpt-5.4", + "-c", + 'model_reasoning_effort="xhigh"', + ], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + 'FORWARDED:exec status --model gpt-5.4 -c model_reasoning_effort="xhigh" -c cli_auth_credentials_store="file"', + ); + }); + it.skipIf(process.platform !== "win32")( "installs Windows codex shell guards to survive shim takeover", () => { @@ -467,6 +759,10 @@ describe("codex bin wrapper", () => { join(repoRootDir, "scripts", "codex-routing.js"), join(scriptDir, "codex-routing.js"), ); + copyFileSync( + join(repoRootDir, "scripts", "codex-bin-resolver.js"), + join(scriptDir, "codex-bin-resolver.js"), + ); writeFileSync( join(globalShimDir, "codex-multi-auth.cmd"), "@ECHO OFF\r\nREM real shim\r\n", @@ -595,6 +891,290 @@ describe("codex bin wrapper", () => { ); }); + it("discovers the real codex bin via npm root fallback for direct script runs on Windows", () => { + const fixtureRoot = createWrapperFixture(); + const fakeGlobalRoot = join(fixtureRoot, "fake-global-node_modules"); + const fakeGlobalBin = createFakeGlobalCodexInstall(fakeGlobalRoot); + const spawnCalls: Array<{ + args: string[]; + command: string; + options: Record; + }> = []; + const resolvedBin = resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + ComSpec: "C:\\Windows\\System32\\cmd.exe", + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + existsSyncImpl: (candidatePath) => candidatePath === fakeGlobalBin, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "win32", + resolvePackageBin: () => null, + spawnSyncImpl: (command, args, options) => { + spawnCalls.push({ + args, + command, + options: options as Record, + }); + return createSpawnSyncSuccess(`${fakeGlobalRoot}\r\n`); + }, + }); + + expect(resolvedBin).toBe(fakeGlobalBin); + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.command).toBe("C:\\Windows\\System32\\cmd.exe"); + expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + expect(spawnCalls[0]?.options).toMatchObject({ + encoding: "utf8", + env: { + ComSpec: "C:\\Windows\\System32\\cmd.exe", + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); + }); + + it("honors uppercase COMSPEC when resolving the Windows npm root fallback", () => { + const fixtureRoot = createWrapperFixture(); + const fakeGlobalRoot = join(fixtureRoot, "fake-global-node_modules-uppercase"); + const fakeGlobalBin = createFakeGlobalCodexInstall(fakeGlobalRoot); + const spawnCalls: Array<{ + args: string[]; + command: string; + options: Record; + }> = []; + const resolvedBin = resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + COMSPEC: "C:\\Windows\\System32\\cmd.exe", + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + existsSyncImpl: (candidatePath) => candidatePath === fakeGlobalBin, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "win32", + resolvePackageBin: () => null, + spawnSyncImpl: (command, args, options) => { + spawnCalls.push({ + args, + command, + options: options as Record, + }); + return createSpawnSyncSuccess(`${fakeGlobalRoot}\r\n`); + }, + }); + + expect(resolvedBin).toBe(fakeGlobalBin); + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.command).toBe("C:\\Windows\\System32\\cmd.exe"); + expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + expect(spawnCalls[0]?.options).toMatchObject({ + encoding: "utf8", + env: { + COMSPEC: "C:\\Windows\\System32\\cmd.exe", + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); + }); + + it("derives cmd.exe from SystemRoot when ComSpec is unavailable", () => { + const fixtureRoot = createWrapperFixture(); + const fakeGlobalRoot = join(fixtureRoot, "fake-global-node_modules-systemroot"); + const fakeGlobalBin = createFakeGlobalCodexInstall(fakeGlobalRoot); + const spawnCalls: Array<{ + args: string[]; + command: string; + options: Record; + }> = []; + const resolvedBin = resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + SystemRoot: "C:\\Windows\\", + npm_config_prefix: "", + }, + existsSyncImpl: (candidatePath) => candidatePath === fakeGlobalBin, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "win32", + resolvePackageBin: () => null, + spawnSyncImpl: (command, args, options) => { + spawnCalls.push({ + args, + command, + options: options as Record, + }); + return createSpawnSyncSuccess(`${fakeGlobalRoot}\r\n`); + }, + }); + + expect(resolvedBin).toBe(fakeGlobalBin); + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.command).toBe("C:\\Windows\\System32\\cmd.exe"); + expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + }); + + it("derives cmd.exe from uppercase SYSTEMROOT when ComSpec is unavailable", () => { + const fixtureRoot = createWrapperFixture(); + const fakeGlobalRoot = join( + fixtureRoot, + "fake-global-node_modules-systemroot-uppercase", + ); + const fakeGlobalBin = createFakeGlobalCodexInstall(fakeGlobalRoot); + const spawnCalls: Array<{ + args: string[]; + command: string; + options: Record; + }> = []; + const resolvedBin = resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + SYSTEMROOT: "C:\\Windows\\", + npm_config_prefix: "", + }, + existsSyncImpl: (candidatePath) => candidatePath === fakeGlobalBin, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "win32", + resolvePackageBin: () => null, + spawnSyncImpl: (command, args, options) => { + spawnCalls.push({ + args, + command, + options: options as Record, + }); + return createSpawnSyncSuccess(`${fakeGlobalRoot}\r\n`); + }, + }); + + expect(resolvedBin).toBe(fakeGlobalBin); + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.command).toBe("C:\\Windows\\System32\\cmd.exe"); + expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + }); + + it("falls back to bare cmd.exe when no Windows shell env vars are set", () => { + const fixtureRoot = createWrapperFixture(); + const spawnCalls: Array<{ + args: string[]; + command: string; + options: Record; + }> = []; + + resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + existsSyncImpl: () => false, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "win32", + resolvePackageBin: () => null, + spawnSyncImpl: (command, args, options) => { + spawnCalls.push({ + args, + command, + options: options as Record, + }); + return createSpawnSyncSuccess(""); + }, + }); + + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.command).toBe("cmd.exe"); + expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + expect(spawnCalls[0]?.options).toMatchObject({ + encoding: "utf8", + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); + }); + + it("discovers the real codex bin via npm root fallback on POSIX", () => { + const fixtureRoot = createWrapperFixture(); + const fakeGlobalRoot = join(fixtureRoot, "fake-global-node_modules-posix"); + const fakeGlobalBin = createFakeGlobalCodexInstall(fakeGlobalRoot); + const spawnCalls: Array<{ + args: string[]; + command: string; + options: Record; + }> = []; + const resolvedBin = resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + existsSyncImpl: (candidatePath) => candidatePath === fakeGlobalBin, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "linux", + resolvePackageBin: () => null, + spawnSyncImpl: (command, args, options) => { + spawnCalls.push({ + args, + command, + options: options as Record, + }); + return createSpawnSyncSuccess(`${fakeGlobalRoot}\n`); + }, + }); + + expect(resolvedBin).toBe(fakeGlobalBin); + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.command).toBe("npm"); + expect(spawnCalls[0]?.args).toEqual(["root", "-g"]); + expect(spawnCalls[0]?.options).toMatchObject({ + encoding: "utf8", + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + stdio: ["ignore", "pipe", "ignore"], + }); + expect(spawnCalls[0]?.options).not.toHaveProperty("windowsHide"); + }); + + it("returns null when npm root lookup throws", () => { + const fixtureRoot = createWrapperFixture(); + const resolvedBin = resolveRealCodexBin({ + argv: ["node", join(fixtureRoot, "scripts", "codex.js")], + env: { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: "", + PREFIX: "", + npm_config_prefix: "", + }, + existsSyncImpl: () => false, + moduleUrl: pathToFileURL(join(fixtureRoot, "scripts", "codex.js")).href, + platform: "linux", + resolvePackageBin: () => null, + spawnSyncImpl: () => { + throw new Error("ENOENT: npm not found"); + }, + }); + + expect(resolvedBin).toBeNull(); + }); + it("handles concurrent wrapper invocations without module-load regressions", async () => { const fixtureRoot = createWrapperFixture(); const fakeBin = createFakeCodexBin(fixtureRoot); From a3c79e0dab8291df6112c0fb793d936ba3a7d288 Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:33:31 +0800 Subject: [PATCH 07/18] fix: harden codex wrapper fallbacks --- scripts/codex-bin-resolver.js | 2 ++ scripts/codex.js | 16 ++++++++++++++-- test/codex-bin-wrapper.test.ts | 23 ++++++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/scripts/codex-bin-resolver.js b/scripts/codex-bin-resolver.js index 31f7e7bd8..ff6e38509 100644 --- a/scripts/codex-bin-resolver.js +++ b/scripts/codex-bin-resolver.js @@ -75,12 +75,14 @@ export function resolveRealCodexBin(options = {}) { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, windowsHide: true, }) : spawnSyncImpl("npm", ["root", "-g"], { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, }); if (rootResult.status === 0) { const globalRoot = rootResult.stdout.trim(); diff --git a/scripts/codex.js b/scripts/codex.js index a85a31446..e4ca8e133 100755 --- a/scripts/codex.js +++ b/scripts/codex.js @@ -2,6 +2,7 @@ import { spawn } from "node:child_process"; import { + chmodSync, copyFileSync, existsSync, mkdirSync, @@ -510,12 +511,23 @@ function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { // Best-effort cleanup only. } }; + const tightenShadowHomePermissions = (path) => { + try { + chmodSync(path, 0o600); + } catch { + // Best-effort only; permission semantics vary by platform. + } + }; try { - writeFileSync(join(shadowCodexHome, "config.toml"), compatConfig, "utf8"); + const compatConfigPath = join(shadowCodexHome, "config.toml"); + writeFileSync(compatConfigPath, compatConfig, "utf8"); + tightenShadowHomePermissions(compatConfigPath); for (const name of ["auth.json", "accounts.json", ".codex-global-state.json"]) { const sourcePath = join(originalCodexHome, name); if (existsSync(sourcePath)) { - copyFileSync(sourcePath, join(shadowCodexHome, name)); + const destinationPath = join(shadowCodexHome, name); + copyFileSync(sourcePath, destinationPath); + tightenShadowHomePermissions(destinationPath); } } } catch (error) { diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index 0bf42cd11..a1f902aa2 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -347,9 +347,13 @@ describe("codex bin wrapper", () => { 'const configPath = path.join(process.env.CODEX_HOME ?? "", "config.toml");', 'const authPath = path.join(process.env.CODEX_HOME ?? "", "auth.json");', 'console.log(`AUTH_EXISTS:${fs.existsSync(authPath)}`);', - 'if (fs.existsSync(authPath)) console.log(`AUTH_JSON:${fs.readFileSync(authPath, "utf8").trim()}`);', + 'if (fs.existsSync(authPath)) {', + ' console.log(`AUTH_JSON:${fs.readFileSync(authPath, "utf8").trim()}`);', + ' console.log(`AUTH_MODE:${(fs.statSync(authPath).mode & 0o777).toString(8)}`);', + '}', 'console.log("CONFIG_START");', 'console.log(fs.readFileSync(configPath, "utf8").trim());', + 'console.log(`CONFIG_MODE:${(fs.statSync(configPath).mode & 0o777).toString(8)}`);', 'console.log("CONFIG_END");', "process.exit(0);", ]); @@ -382,8 +386,14 @@ describe("codex bin wrapper", () => { expect(output).toContain("CODEX_MULTI_AUTH_DIR_JSON:null"); expect(output).toContain("AUTH_EXISTS:true"); expect(output).toContain("AUTH_JSON:{}"); + expect(output).toContain("AUTH_MODE:"); expect(output).toContain('model_reasoning_effort = "high"'); + expect(output).toContain("CONFIG_MODE:"); expect(output).not.toContain('model_reasoning_effort = "xhigh"'); + if (process.platform !== "win32") { + expect(output).toContain("AUTH_MODE:600"); + expect(output).toContain("CONFIG_MODE:600"); + } }); it("cleans up compatibility shadow homes when staging fails", () => { @@ -410,6 +420,7 @@ describe("codex bin wrapper", () => { CODEX_HOME: originalHome, TMP: controlledTmp, TEMP: controlledTmp, + TMPDIR: controlledTmp, }, ); @@ -935,6 +946,7 @@ describe("codex bin wrapper", () => { npm_config_prefix: "", }, stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, windowsHide: true, }); }); @@ -983,6 +995,7 @@ describe("codex bin wrapper", () => { npm_config_prefix: "", }, stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, windowsHide: true, }); }); @@ -1022,6 +1035,9 @@ describe("codex bin wrapper", () => { expect(spawnCalls).toHaveLength(1); expect(spawnCalls[0]?.command).toBe("C:\\Windows\\System32\\cmd.exe"); expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + expect(spawnCalls[0]?.options).toMatchObject({ + timeout: 5000, + }); }); it("derives cmd.exe from uppercase SYSTEMROOT when ComSpec is unavailable", () => { @@ -1062,6 +1078,9 @@ describe("codex bin wrapper", () => { expect(spawnCalls).toHaveLength(1); expect(spawnCalls[0]?.command).toBe("C:\\Windows\\System32\\cmd.exe"); expect(spawnCalls[0]?.args).toEqual(["/d", "/s", "/c", "npm root -g"]); + expect(spawnCalls[0]?.options).toMatchObject({ + timeout: 5000, + }); }); it("falls back to bare cmd.exe when no Windows shell env vars are set", () => { @@ -1104,6 +1123,7 @@ describe("codex bin wrapper", () => { npm_config_prefix: "", }, stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, windowsHide: true, }); }); @@ -1150,6 +1170,7 @@ describe("codex bin wrapper", () => { npm_config_prefix: "", }, stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, }); expect(spawnCalls[0]?.options).not.toHaveProperty("windowsHide"); }); From 2d2b2e68589585bf484674668a3487c361acd803 Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 11:35:27 +0800 Subject: [PATCH 08/18] fix: harden config validation and settings recovery --- lib/config.ts | 131 +++++++++++++++++++++++++++++----- lib/unified-settings.ts | 108 +++++++++++++++++++++++----- test/config-save.test.ts | 10 +-- test/plugin-config.test.ts | 29 +++++++- test/unified-settings.test.ts | 56 ++++++++++++--- 5 files changed, 284 insertions(+), 50 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index c68597fc7..73483716b 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -199,6 +199,11 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = { preemptiveQuotaMaxDeferralMs: 2 * 60 * 60_000, }; +const PLUGIN_CONFIG_FIELD_SCHEMAS = PluginConfigSchema.shape; +const PLUGIN_CONFIG_KEYS = Object.keys(DEFAULT_PLUGIN_CONFIG) as Array< + keyof PluginConfig +>; + /** * Return a shallow copy of the default plugin configuration. * @@ -238,6 +243,10 @@ export function loadPluginConfig(): PluginConfig { sourceKind = "file"; } + const normalizedUserConfig = sanitizePluginConfigRecord(userConfig, { + warnOnInvalid: true, + }); + const hasFallbackEnvOverride = process.env.CODEX_AUTH_FALLBACK_UNSUPPORTED_MODEL !== undefined || process.env.CODEX_AUTH_FALLBACK_GPT53_TO_GPT52 !== undefined; @@ -255,16 +264,9 @@ export function loadPluginConfig(): PluginConfig { } } - const schemaErrors = getValidationErrors(PluginConfigSchema, userConfig); - if (schemaErrors.length > 0) { - logConfigWarnOnce( - `Plugin config validation warnings: ${schemaErrors.slice(0, 3).join(", ")}`, - ); - } - if ( sourceKind === "file" && - isRecord(userConfig) && + normalizedUserConfig !== null && (process.env.CODEX_MULTI_AUTH_CONFIG_PATH ?? "").trim().length === 0 ) { logConfigWarnOnce( @@ -274,7 +276,7 @@ export function loadPluginConfig(): PluginConfig { return { ...DEFAULT_PLUGIN_CONFIG, - ...(userConfig as Partial), + ...(normalizedUserConfig ?? {}), }; } catch (error) { const configPath = resolvePluginConfigPath() ?? CONFIG_PATH; @@ -285,6 +287,62 @@ export function loadPluginConfig(): PluginConfig { } } +function sanitizePluginConfigRecord( + data: unknown, + options?: { warnOnInvalid?: boolean }, +): Partial | null { + if (!isRecord(data)) { + return null; + } + + if (options?.warnOnInvalid) { + const schemaErrors = getValidationErrors(PluginConfigSchema, data); + if (schemaErrors.length > 0) { + logConfigWarnOnce( + `Plugin config validation warnings: ${schemaErrors.slice(0, 3).join(", ")}`, + ); + } + } + + const sanitized: Record = {}; + for (const key of PLUGIN_CONFIG_KEYS) { + const value = data[key]; + if (value === undefined) { + continue; + } + const schema = PLUGIN_CONFIG_FIELD_SCHEMAS[key]; + const result = schema.safeParse(value); + if (result.success) { + sanitized[String(key)] = result.data; + } + } + + return sanitized as Partial; +} + +function sanitizeStoredPluginConfigRecord( + data: unknown, +): Record { + if (!isRecord(data)) { + return {}; + } + + const sanitized: Record = {}; + for (const [key, value] of Object.entries(data)) { + if (!PLUGIN_CONFIG_KEYS.includes(key as keyof PluginConfig)) { + sanitized[key] = value; + continue; + } + const schema = PLUGIN_CONFIG_FIELD_SCHEMAS[key as keyof PluginConfig]; + const result = schema.safeParse(value); + if (result.success) { + sanitized[key] = result.data; + } + } + + return sanitized; +} + /** * Remove a leading UTF‑8 byte order mark (BOM) from the given string if present. * @@ -443,7 +501,8 @@ function resolveStoredPluginConfigRecord(): { function sanitizePluginConfigForSave( config: Partial, ): Record { - const entries = Object.entries(config as Record); + const normalized = sanitizePluginConfigRecord(config as Record); + const entries = Object.entries((normalized ?? {}) as Record); const sanitized: Record = {}; for (const [key, value] of entries) { if (value === undefined) continue; @@ -478,8 +537,11 @@ export async function savePluginConfig( if (envPath.length > 0) { await withConfigSaveLock(envPath, async () => { + const existingConfig = sanitizeStoredPluginConfigRecord( + readConfigRecordFromPath(envPath), + ); const merged = { - ...(readConfigRecordFromPath(envPath) ?? {}), + ...(existingConfig ?? {}), ...sanitizedPatch, }; await writeJsonFileAtomicWithRetry(envPath, merged); @@ -489,11 +551,16 @@ export async function savePluginConfig( const unifiedPath = getUnifiedSettingsPath(); await withConfigSaveLock(unifiedPath, async () => { - const unifiedConfig = loadUnifiedPluginConfigSync(); + const unifiedConfig = sanitizeStoredPluginConfigRecord( + loadUnifiedPluginConfigSync(), + ); const legacyPath = unifiedConfig ? null : resolvePluginConfigPath(); + const legacyConfig = legacyPath + ? sanitizeStoredPluginConfigRecord(readConfigRecordFromPath(legacyPath)) + : null; const merged = { ...(unifiedConfig ?? - (legacyPath ? readConfigRecordFromPath(legacyPath) : null) ?? + legacyConfig ?? {}), ...sanitizedPatch, }; @@ -510,12 +577,30 @@ export async function savePluginConfig( */ function parseBooleanEnv(value: string | undefined): boolean | undefined { if (value === undefined) return undefined; - return value === "1"; + const normalized = value.trim().toLowerCase(); + if (normalized.length === 0) return undefined; + if ( + normalized === "1" || + normalized === "true" || + normalized === "yes" + ) { + return true; + } + if ( + normalized === "0" || + normalized === "false" || + normalized === "no" + ) { + return false; + } + return undefined; } function parseNumberEnv(value: string | undefined): number | undefined { if (value === undefined) return undefined; - const parsed = Number(value); + const trimmed = value.trim(); + if (trimmed.length === 0) return undefined; + const parsed = Number(trimmed); if (!Number.isFinite(parsed)) return undefined; return parsed; } @@ -544,7 +629,13 @@ function resolveBooleanSetting( configValue: boolean | undefined, defaultValue: boolean, ): boolean { - const envValue = parseBooleanEnv(process.env[envName]); + const rawEnvValue = process.env[envName]; + const envValue = parseBooleanEnv(rawEnvValue); + if (rawEnvValue !== undefined && envValue === undefined) { + logConfigWarnOnce( + `Ignoring invalid boolean env ${envName}=${JSON.stringify(rawEnvValue)}. Expected 0/1, true/false, or yes/no.`, + ); + } if (envValue !== undefined) return envValue; return configValue ?? defaultValue; } @@ -566,7 +657,13 @@ function resolveNumberSetting( defaultValue: number, options?: { min?: number; max?: number }, ): number { - const envValue = parseNumberEnv(process.env[envName]); + const rawEnvValue = process.env[envName]; + const envValue = parseNumberEnv(rawEnvValue); + if (rawEnvValue !== undefined && envValue === undefined) { + logConfigWarnOnce( + `Ignoring invalid numeric env ${envName}=${JSON.stringify(rawEnvValue)}. Expected a finite number.`, + ); + } const candidate = envValue ?? configValue ?? defaultValue; const min = options?.min ?? Number.NEGATIVE_INFINITY; const max = options?.max ?? Number.POSITIVE_INFINITY; diff --git a/lib/unified-settings.ts b/lib/unified-settings.ts index ff63d8942..356ad2540 100644 --- a/lib/unified-settings.ts +++ b/lib/unified-settings.ts @@ -1,4 +1,5 @@ import { + copyFileSync, existsSync, mkdirSync, renameSync, @@ -16,6 +17,7 @@ type JsonRecord = Record; export const UNIFIED_SETTINGS_VERSION = 1 as const; const UNIFIED_SETTINGS_PATH = join(getCodexMultiAuthDir(), "settings.json"); +const UNIFIED_SETTINGS_BACKUP_PATH = `${UNIFIED_SETTINGS_PATH}.bak`; const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]); let settingsWriteQueue: Promise = Promise.resolve(); @@ -45,6 +47,64 @@ function cloneRecord(value: unknown): JsonRecord | null { return { ...value }; } +function parseSettingsRecord(content: string): JsonRecord { + const parsed = cloneRecord(JSON.parse(content)); + if (!parsed) { + throw new Error("Unified settings must contain a JSON object at the root."); + } + return parsed; +} + +function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null { + if (!existsSync(filePath)) { + return null; + } + return parseSettingsRecord(readFileSync(filePath, "utf8")); +} + +async function readSettingsRecordAsyncFromPath( + filePath: string, +): Promise { + if (!existsSync(filePath)) { + return null; + } + return parseSettingsRecord(await fs.readFile(filePath, "utf8")); +} + +function trySnapshotUnifiedSettingsBackupSync(): void { + if (!existsSync(UNIFIED_SETTINGS_PATH)) { + return; + } + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + copyFileSync(UNIFIED_SETTINGS_PATH, UNIFIED_SETTINGS_BACKUP_PATH); + return; + } catch (error) { + if (!isRetryableFsError(error) || attempt >= 4) { + return; + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * 2 ** attempt); + } + } +} + +async function trySnapshotUnifiedSettingsBackupAsync(): Promise { + if (!existsSync(UNIFIED_SETTINGS_PATH)) { + return; + } + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + await fs.copyFile(UNIFIED_SETTINGS_PATH, UNIFIED_SETTINGS_BACKUP_PATH); + return; + } catch (error) { + if (!isRetryableFsError(error) || attempt >= 4) { + return; + } + await sleep(10 * 2 ** attempt); + } + } +} + /** * Reads and parses the unified settings JSON file from disk. * @@ -56,16 +116,22 @@ function cloneRecord(value: unknown): JsonRecord | null { * - Sensitive data: this function performs no token or secret redaction; any sensitive values present in the file are returned as-is and callers are responsible for redaction before logging or external exposure. */ function readSettingsRecordSync(): JsonRecord | null { - if (!existsSync(UNIFIED_SETTINGS_PATH)) { - return null; + try { + const primaryRecord = readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_PATH); + if (primaryRecord) { + return primaryRecord; + } + } catch (error) { + const backupRecord = readSettingsRecordSyncFromPath( + UNIFIED_SETTINGS_BACKUP_PATH, + ); + if (backupRecord) { + return backupRecord; + } + throw error; } - const raw = readFileSync(UNIFIED_SETTINGS_PATH, "utf8"); - const parsed = cloneRecord(JSON.parse(raw)); - if (!parsed) { - throw new Error("Unified settings must contain a JSON object at the root."); - } - return parsed; + return readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); } /** @@ -76,16 +142,24 @@ function readSettingsRecordSync(): JsonRecord | null { * @returns The parsed settings record as an object clone, or `null` if unavailable or invalid. */ async function readSettingsRecordAsync(): Promise { - if (!existsSync(UNIFIED_SETTINGS_PATH)) { - return null; + try { + const primaryRecord = await readSettingsRecordAsyncFromPath( + UNIFIED_SETTINGS_PATH, + ); + if (primaryRecord) { + return primaryRecord; + } + } catch (error) { + const backupRecord = await readSettingsRecordAsyncFromPath( + UNIFIED_SETTINGS_BACKUP_PATH, + ); + if (backupRecord) { + return backupRecord; + } + throw error; } - const raw = await fs.readFile(UNIFIED_SETTINGS_PATH, "utf8"); - const parsed = cloneRecord(JSON.parse(raw)); - if (!parsed) { - throw new Error("Unified settings must contain a JSON object at the root."); - } - return parsed; + return readSettingsRecordAsyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); } /** @@ -125,6 +199,7 @@ function writeSettingsRecordSync(record: JsonRecord): void { const payload = normalizeForWrite(record); const data = `${JSON.stringify(payload, null, 2)}\n`; const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.tmp`; + trySnapshotUnifiedSettingsBackupSync(); writeFileSync(tempPath, data, "utf8"); let moved = false; try { @@ -176,6 +251,7 @@ async function writeSettingsRecordAsync(record: JsonRecord): Promise { const payload = normalizeForWrite(record); const data = `${JSON.stringify(payload, null, 2)}\n`; const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`; + await trySnapshotUnifiedSettingsBackupAsync(); await fs.writeFile(tempPath, data, "utf8"); let moved = false; try { diff --git a/test/config-save.test.ts b/test/config-save.test.ts index 2064faebd..b33ed7375 100644 --- a/test/config-save.test.ts +++ b/test/config-save.test.ts @@ -135,11 +135,11 @@ describe("plugin config save paths", () => { parallelProbingMaxConcurrency: 7, }); - const loaded = loadPluginConfig(); - expect(loaded.codexMode).toBe(false); - expect(loaded.parallelProbing).toBe(true); - expect(loaded.parallelProbingMaxConcurrency).toBe(7); - }); + const loaded = loadPluginConfig(); + expect(loaded.codexMode).toBe(false); + expect(loaded.parallelProbing).toBe(true); + expect(loaded.parallelProbingMaxConcurrency).toBe(2); + }); it("resolves parallel probing settings and clamps concurrency", async () => { const { getParallelProbing, getParallelProbingMaxConcurrency } = diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts index 59c5d2336..d3d257000 100644 --- a/test/plugin-config.test.ts +++ b/test/plugin-config.test.ts @@ -661,13 +661,13 @@ describe('Plugin Configuration', () => { expect(result).toBe(false); }); - it('should handle env var with any value other than "1" as false', () => { - process.env.CODEX_MODE = 'false'; + it('should ignore invalid env values and fall back to config/default', () => { + process.env.CODEX_MODE = 'maybe'; const config: PluginConfig = { codexMode: true }; const result = getCodexMode(config); - expect(result).toBe(false); + expect(result).toBe(true); }); it('should use config codexMode=true when explicitly set', () => { @@ -990,6 +990,23 @@ describe('Plugin Configuration', () => { expect.stringContaining('Plugin config validation warnings') ); }); + + it('drops invalid persisted values while keeping valid config keys', () => { + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue(JSON.stringify({ + codexMode: 'not-a-boolean', + fetchTimeoutMs: 'oops', + streamStallTimeoutMs: 30000, + backgroundResponses: true, + })); + + const config = loadPluginConfig(); + + expect(config.codexMode).toBe(true); + expect(config.fetchTimeoutMs).toBe(60_000); + expect(config.streamStallTimeoutMs).toBe(30000); + expect(config.backgroundResponses).toBe(true); + }); }); describe('resolveNumberSetting without min option', () => { @@ -1015,6 +1032,12 @@ describe('Plugin Configuration', () => { expect(getFetchTimeoutMs(config)).toBe(120000); }); + it('should ignore empty numeric env values and fall back to config/default', () => { + process.env.CODEX_AUTH_FETCH_TIMEOUT_MS = ''; + expect(getFetchTimeoutMs({ fetchTimeoutMs: 90000 })).toBe(90000); + delete process.env.CODEX_AUTH_FETCH_TIMEOUT_MS; + }); + it('should read stream stall timeout from env', () => { process.env.CODEX_AUTH_STREAM_STALL_TIMEOUT_MS = '30000'; expect(getStreamStallTimeoutMs({})).toBe(30000); diff --git a/test/unified-settings.test.ts b/test/unified-settings.test.ts index 0d749cbd7..2459214f2 100644 --- a/test/unified-settings.test.ts +++ b/test/unified-settings.test.ts @@ -71,6 +71,46 @@ describe("unified settings", () => { expect(await loadUnifiedDashboardSettings()).toBeNull(); }); + it("recovers plugin config from backup when primary settings file is invalid", async () => { + const { + getUnifiedSettingsPath, + saveUnifiedPluginConfig, + loadUnifiedPluginConfigSync, + } = await import("../lib/unified-settings.js"); + + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 45_000 }); + await saveUnifiedPluginConfig({ codexMode: false, fetchTimeoutMs: 90_000 }); + await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8"); + + expect(loadUnifiedPluginConfigSync()).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + }); + + it("recovers dashboard settings from backup when primary settings file is invalid", async () => { + const { + getUnifiedSettingsPath, + saveUnifiedDashboardSettings, + loadUnifiedDashboardSettings, + } = await import("../lib/unified-settings.js"); + + await saveUnifiedDashboardSettings({ + menuShowLastUsed: true, + uiThemePreset: "green", + }); + await saveUnifiedDashboardSettings({ + menuShowLastUsed: false, + uiThemePreset: "blue", + }); + await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8"); + + expect(await loadUnifiedDashboardSettings()).toEqual({ + menuShowLastUsed: true, + uiThemePreset: "green", + }); + }); + it("returns null when dashboard settings file is missing", async () => { const { loadUnifiedDashboardSettings } = await import( "../lib/unified-settings.js" @@ -379,7 +419,7 @@ describe("unified settings", () => { }); }); - it("refuses overwriting settings sections when a read fails", async () => { + it("preserves settings sections when a primary read fails and backup recovery is available", async () => { const { saveUnifiedPluginConfig, saveUnifiedDashboardSettings, @@ -400,12 +440,10 @@ describe("unified settings", () => { }); try { - await expect( - saveUnifiedDashboardSettings({ - menuShowLastUsed: true, - uiThemePreset: "yellow", - }), - ).rejects.toThrow(); + await saveUnifiedDashboardSettings({ + menuShowLastUsed: true, + uiThemePreset: "yellow", + }); } finally { readSpy.mockRestore(); } @@ -420,8 +458,8 @@ describe("unified settings", () => { fetchTimeoutMs: 70_000, }); expect(parsed.dashboardDisplaySettings).toEqual({ - menuShowLastUsed: false, - uiThemePreset: "blue", + menuShowLastUsed: true, + uiThemePreset: "yellow", }); }); }); From 78e9e73b727c91d2b07181d8a784c2ebaf8007e8 Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 11:36:01 +0800 Subject: [PATCH 09/18] fix: recover flagged account backups safely --- lib/storage/flagged-storage-io.ts | 56 +++++++++++++++++++++++++++-- test/storage-flagged.test.ts | 5 +-- test/storage-recovery-paths.test.ts | 50 ++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/lib/storage/flagged-storage-io.ts b/lib/storage/flagged-storage-io.ts index 1788da2b7..376aef60a 100644 --- a/lib/storage/flagged-storage-io.ts +++ b/lib/storage/flagged-storage-io.ts @@ -2,6 +2,10 @@ import { existsSync, promises as fs } from "node:fs"; import { dirname } from "node:path"; import type { FlaggedAccountStorageV1 } from "../storage.js"; +function getFlaggedBackupPaths(path: string): string[] { + return [`${path}.bak`, `${path}.bak.1`, `${path}.bak.2`]; +} + export async function loadFlaggedAccountsState(params: { path: string; legacyPath: string; @@ -12,6 +16,34 @@ export async function loadFlaggedAccountsState(params: { logInfo: (message: string, details: Record) => void; }): Promise { const empty: FlaggedAccountStorageV1 = { version: 1, accounts: [] }; + if (existsSync(params.resetMarkerPath)) { + return empty; + } + const loadFlaggedBackup = async (): Promise => { + for (const backupPath of getFlaggedBackupPaths(params.path)) { + if (!existsSync(backupPath)) { + continue; + } + try { + const backupContent = await fs.readFile(backupPath, "utf-8"); + const backupData = JSON.parse(backupContent) as unknown; + const recovered = params.normalizeFlaggedStorage(backupData); + params.logInfo("Recovered flagged account storage from backup", { + from: backupPath, + to: params.path, + accounts: recovered.accounts.length, + }); + return recovered; + } catch (backupError) { + params.logError("Failed to recover flagged account storage from backup", { + from: backupPath, + to: params.path, + error: String(backupError), + }); + } + } + return null; + }; try { const content = await fs.readFile(params.path, "utf-8"); @@ -28,10 +60,15 @@ export async function loadFlaggedAccountsState(params: { path: params.path, error: String(error), }); - return empty; + return (await loadFlaggedBackup()) ?? empty; } } + const recoveredBackup = await loadFlaggedBackup(); + if (recoveredBackup) { + return recoveredBackup; + } + if (!existsSync(params.legacyPath)) { return empty; } @@ -129,6 +166,7 @@ export async function clearFlaggedAccountsOnDisk(params: { backupPaths: string[]; logError: (message: string, details: Record) => void; }): Promise { + let keepResetMarker = false; try { await fs.writeFile(params.markerPath, "reset", { encoding: "utf-8", @@ -145,7 +183,6 @@ export async function clearFlaggedAccountsOnDisk(params: { for (const candidate of [ params.path, ...params.backupPaths, - params.markerPath, ]) { try { await fs.unlink(candidate); @@ -159,6 +196,21 @@ export async function clearFlaggedAccountsOnDisk(params: { if (candidate === params.path) { throw error; } + keepResetMarker = true; + } + } + } + if (!keepResetMarker) { + try { + await fs.unlink(params.markerPath); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ENOENT") { + params.logError("Failed to clear flagged account storage", { + path: params.markerPath, + error: String(error), + }); + throw error; } } } diff --git a/test/storage-flagged.test.ts b/test/storage-flagged.test.ts index 8effd3df4..bfd4de24a 100644 --- a/test/storage-flagged.test.ts +++ b/test/storage-flagged.test.ts @@ -292,7 +292,7 @@ describe("flagged account storage", () => { unlinkSpy.mockRestore(); }); - it("does not recover flagged backups when the primary file exists but read fails", async () => { + it("recovers flagged backups when the primary file exists but read fails", async () => { await saveFlaggedAccounts({ version: 1, accounts: [ @@ -334,7 +334,8 @@ describe("flagged account storage", () => { }); const flagged = await loadFlaggedAccounts(); - expect(flagged.accounts).toHaveLength(0); + expect(flagged.accounts).toHaveLength(1); + expect(flagged.accounts[0]?.refreshToken).toBe("primary-flagged"); expect(existsSync(flaggedPath)).toBe(true); readSpy.mockRestore(); diff --git a/test/storage-recovery-paths.test.ts b/test/storage-recovery-paths.test.ts index 264bf4946..0bbaad6d8 100644 --- a/test/storage-recovery-paths.test.ts +++ b/test/storage-recovery-paths.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "node:os"; import { removeWithRetry } from "./helpers/remove-with-retry.js"; import { loadAccounts, + loadFlaggedAccounts, getBackupMetadata, saveAccounts, setStorageBackupEnabled, @@ -109,6 +110,55 @@ describe("storage recovery paths", () => { expect(persisted.accounts?.[0]?.accountId).toBe("from-backup"); }); + it("recovers flagged accounts from backup file when primary storage is missing", async () => { + const flaggedPath = join(workDir, "openai-codex-flagged-accounts.json"); + await fs.writeFile( + `${flaggedPath}.bak`, + JSON.stringify({ + version: 1, + accounts: [ + { + refreshToken: "flagged-refresh", + email: "flagged@example.com", + addedAt: 7, + lastUsed: 7, + flaggedAt: 7, + }, + ], + }), + "utf-8", + ); + + const recovered = await loadFlaggedAccounts(); + expect(recovered.accounts).toHaveLength(1); + expect(recovered.accounts[0]?.email).toBe("flagged@example.com"); + }); + + it("recovers flagged accounts from backup file when primary storage is unreadable", async () => { + const flaggedPath = join(workDir, "openai-codex-flagged-accounts.json"); + await fs.writeFile(flaggedPath, "{broken-primary", "utf-8"); + await fs.writeFile( + `${flaggedPath}.bak`, + JSON.stringify({ + version: 1, + accounts: [ + { + refreshToken: "flagged-refresh-2", + email: "flagged2@example.com", + addedAt: 8, + lastUsed: 8, + flaggedAt: 8, + }, + ], + }), + "utf-8", + ); + + const recovered = await loadFlaggedAccounts(); + expect(recovered.accounts).toHaveLength(1); + expect(recovered.accounts[0]?.email).toBe("flagged2@example.com"); + }); + it("falls back to historical backup snapshots when the latest backup is unreadable", async () => { await fs.writeFile(storagePath, "{broken-primary", "utf-8"); await fs.writeFile(`${storagePath}.bak`, "{broken-latest-backup", "utf-8"); From 248f6c5bb08a897981d30e04080bb40cb55bdf6b Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 11:37:23 +0800 Subject: [PATCH 10/18] docs: document settings and flagged backup recovery --- docs/reference/storage-paths.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/storage-paths.md b/docs/reference/storage-paths.md index 186ab1f54..6fde1577a 100644 --- a/docs/reference/storage-paths.md +++ b/docs/reference/storage-paths.md @@ -21,6 +21,7 @@ Override root: | File | Default path | | --- | --- | | Unified settings | `~/.codex/multi-auth/settings.json` | +| Unified settings backup | `~/.codex/multi-auth/settings.json.bak` | | Accounts | `~/.codex/multi-auth/openai-codex-accounts.json` | | Accounts backup | `~/.codex/multi-auth/openai-codex-accounts.json.bak` | | Accounts WAL | `~/.codex/multi-auth/openai-codex-accounts.json.wal` | @@ -48,6 +49,8 @@ Compatibility note: Backup metadata: - `getBackupMetadata()` reports deterministic snapshot lists for the canonical account pool (primary, WAL, `.bak`, `.bak.1`, `.bak.2`, and discovered manual backups) and flagged-account state (primary, `.bak`, `.bak.1`, `.bak.2`, and discovered manual backups). Cache-like artifacts and `.reset-intent` markers are excluded from recovery candidates. +- `settings.json.bak` stores the last valid unified settings snapshot before each write and is used as a recovery fallback when `settings.json` is unreadable. +- Flagged-account backup recovery is suppressed whenever the flagged reset marker is still present, so partial clears cannot revive previously cleared flagged entries. --- From e624db9a25c80a0c5e6dda7db0abdbc65a24ea5c Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 11:49:47 +0800 Subject: [PATCH 11/18] fix: ignore corrupt unified settings backups --- lib/unified-settings.ts | 28 ++++++++++++++++++++-------- test/unified-settings.test.ts | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/unified-settings.ts b/lib/unified-settings.ts index 356ad2540..cbd5a9291 100644 --- a/lib/unified-settings.ts +++ b/lib/unified-settings.ts @@ -71,6 +71,22 @@ async function readSettingsRecordAsyncFromPath( return parseSettingsRecord(await fs.readFile(filePath, "utf8")); } +function readSettingsBackupSync(): JsonRecord | null { + try { + return readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); + } catch { + return null; + } +} + +async function readSettingsBackupAsync(): Promise { + try { + return await readSettingsRecordAsyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); + } catch { + return null; + } +} + function trySnapshotUnifiedSettingsBackupSync(): void { if (!existsSync(UNIFIED_SETTINGS_PATH)) { return; @@ -122,16 +138,14 @@ function readSettingsRecordSync(): JsonRecord | null { return primaryRecord; } } catch (error) { - const backupRecord = readSettingsRecordSyncFromPath( - UNIFIED_SETTINGS_BACKUP_PATH, - ); + const backupRecord = readSettingsBackupSync(); if (backupRecord) { return backupRecord; } throw error; } - return readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); + return readSettingsBackupSync(); } /** @@ -150,16 +164,14 @@ async function readSettingsRecordAsync(): Promise { return primaryRecord; } } catch (error) { - const backupRecord = await readSettingsRecordAsyncFromPath( - UNIFIED_SETTINGS_BACKUP_PATH, - ); + const backupRecord = await readSettingsBackupAsync(); if (backupRecord) { return backupRecord; } throw error; } - return readSettingsRecordAsyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); + return readSettingsBackupAsync(); } /** diff --git a/test/unified-settings.test.ts b/test/unified-settings.test.ts index 2459214f2..f29235ea1 100644 --- a/test/unified-settings.test.ts +++ b/test/unified-settings.test.ts @@ -71,6 +71,20 @@ describe("unified settings", () => { expect(await loadUnifiedDashboardSettings()).toBeNull(); }); + it("returns null sections when both primary and backup settings files are invalid", async () => { + const { + getUnifiedSettingsPath, + loadUnifiedPluginConfigSync, + loadUnifiedDashboardSettings, + } = await import("../lib/unified-settings.js"); + + await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8"); + await fs.writeFile(`${getUnifiedSettingsPath()}.bak`, "{ invalid backup", "utf8"); + + expect(loadUnifiedPluginConfigSync()).toBeNull(); + expect(await loadUnifiedDashboardSettings()).toBeNull(); + }); + it("recovers plugin config from backup when primary settings file is invalid", async () => { const { getUnifiedSettingsPath, From dab4f437f33140c37f6c37d7f1ab33fd3f8d66e4 Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 12:27:01 +0800 Subject: [PATCH 12/18] fix: tighten config and recovery review follow-ups --- docs/reference/storage-paths.md | 6 ++ lib/config.ts | 28 +++-- lib/storage/flagged-storage-io.ts | 6 ++ lib/unified-settings.ts | 75 +++++++++++++- test/config-save.test.ts | 29 ++++++ test/plugin-config.test.ts | 5 +- test/storage-flagged.test.ts | 37 +++++++ test/storage-recovery-paths.test.ts | 41 +++++++- test/unified-settings.test.ts | 155 +++++++++++++++++++++++++++- 9 files changed, 368 insertions(+), 14 deletions(-) diff --git a/docs/reference/storage-paths.md b/docs/reference/storage-paths.md index 6fde1577a..e8f0e8fdd 100644 --- a/docs/reference/storage-paths.md +++ b/docs/reference/storage-paths.md @@ -52,6 +52,12 @@ Backup metadata: - `settings.json.bak` stores the last valid unified settings snapshot before each write and is used as a recovery fallback when `settings.json` is unreadable. - Flagged-account backup recovery is suppressed whenever the flagged reset marker is still present, so partial clears cannot revive previously cleared flagged entries. +Upgrade note: + +- Restore workflows now distinguish between unreadable state and intentionally cleared state. `settings.json.bak` is only used when `settings.json` exists but cannot be read, while flagged-account backups stay suppressed whenever the reset marker survives a partial clear. +- Operators validating a restore or clear flow should use `codex auth verify-flagged`, `codex auth fix --dry-run`, and `codex auth doctor --fix` to confirm what will be recovered, what stays cleared, and whether manual repair is still needed. +- Maintainers validating the on-disk upgrade behavior can run `npm run build` plus `npm test -- --run test/unified-settings.test.ts test/storage-recovery-paths.test.ts test/storage-flagged.test.ts` before shipping backup or restore changes. + --- ## Project-Scoped Account Paths diff --git a/lib/config.ts b/lib/config.ts index 73483716b..fe87c22b8 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -320,11 +320,18 @@ function sanitizePluginConfigRecord( return sanitized as Partial; } +/** + * Sanitize a stored plugin-config record while preserving unknown keys. + * + * Known plugin-config keys are schema-validated before they are merged back + * into runtime state. Unknown keys are kept so legacy and forward-compatible + * settings survive env-path saves. + */ function sanitizeStoredPluginConfigRecord( data: unknown, -): Record { +): Record | null { if (!isRecord(data)) { - return {}; + return null; } const sanitized: Record = {}; @@ -551,10 +558,11 @@ export async function savePluginConfig( const unifiedPath = getUnifiedSettingsPath(); await withConfigSaveLock(unifiedPath, async () => { + const unifiedConfigRecord = loadUnifiedPluginConfigSync(); const unifiedConfig = sanitizeStoredPluginConfigRecord( - loadUnifiedPluginConfigSync(), + unifiedConfigRecord, ); - const legacyPath = unifiedConfig ? null : resolvePluginConfigPath(); + const legacyPath = unifiedConfigRecord ? null : resolvePluginConfigPath(); const legacyConfig = legacyPath ? sanitizeStoredPluginConfigRecord(readConfigRecordFromPath(legacyPath)) : null; @@ -631,7 +639,11 @@ function resolveBooleanSetting( ): boolean { const rawEnvValue = process.env[envName]; const envValue = parseBooleanEnv(rawEnvValue); - if (rawEnvValue !== undefined && envValue === undefined) { + if ( + rawEnvValue !== undefined && + rawEnvValue.trim().length > 0 && + envValue === undefined + ) { logConfigWarnOnce( `Ignoring invalid boolean env ${envName}=${JSON.stringify(rawEnvValue)}. Expected 0/1, true/false, or yes/no.`, ); @@ -659,7 +671,11 @@ function resolveNumberSetting( ): number { const rawEnvValue = process.env[envName]; const envValue = parseNumberEnv(rawEnvValue); - if (rawEnvValue !== undefined && envValue === undefined) { + if ( + rawEnvValue !== undefined && + rawEnvValue.trim().length > 0 && + envValue === undefined + ) { logConfigWarnOnce( `Ignoring invalid numeric env ${envName}=${JSON.stringify(rawEnvValue)}. Expected a finite number.`, ); diff --git a/lib/storage/flagged-storage-io.ts b/lib/storage/flagged-storage-io.ts index 376aef60a..068c3f06e 100644 --- a/lib/storage/flagged-storage-io.ts +++ b/lib/storage/flagged-storage-io.ts @@ -2,6 +2,9 @@ import { existsSync, promises as fs } from "node:fs"; import { dirname } from "node:path"; import type { FlaggedAccountStorageV1 } from "../storage.js"; +/** + * Return the ordered backup paths consulted for flagged-account recovery. + */ function getFlaggedBackupPaths(path: string): string[] { return [`${path}.bak`, `${path}.bak.1`, `${path}.bak.2`]; } @@ -28,6 +31,9 @@ export async function loadFlaggedAccountsState(params: { const backupContent = await fs.readFile(backupPath, "utf-8"); const backupData = JSON.parse(backupContent) as unknown; const recovered = params.normalizeFlaggedStorage(backupData); + if (existsSync(params.resetMarkerPath)) { + return empty; + } params.logInfo("Recovered flagged account storage from backup", { from: backupPath, to: params.path, diff --git a/lib/unified-settings.ts b/lib/unified-settings.ts index cbd5a9291..379621971 100644 --- a/lib/unified-settings.ts +++ b/lib/unified-settings.ts @@ -19,7 +19,9 @@ export const UNIFIED_SETTINGS_VERSION = 1 as const; const UNIFIED_SETTINGS_PATH = join(getCodexMultiAuthDir(), "settings.json"); const UNIFIED_SETTINGS_BACKUP_PATH = `${UNIFIED_SETTINGS_PATH}.bak`; const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]); +const TRANSIENT_READ_FS_CODES = new Set(["EBUSY", "EAGAIN"]); let settingsWriteQueue: Promise = Promise.resolve(); +let backupDerivedReadPending = false; function isRetryableFsError(error: unknown): boolean { const code = (error as NodeJS.ErrnoException | undefined)?.code; @@ -55,6 +57,12 @@ function parseSettingsRecord(content: string): JsonRecord { return parsed; } +/** + * Read a unified-settings record from a specific path. + * + * Returns `null` when the file is absent and throws when the file exists but + * cannot be parsed into the expected object shape. + */ function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null { if (!existsSync(filePath)) { return null; @@ -62,6 +70,9 @@ function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null { return parseSettingsRecord(readFileSync(filePath, "utf8")); } +/** + * Async variant of `readSettingsRecordSyncFromPath`. + */ async function readSettingsRecordAsyncFromPath( filePath: string, ): Promise { @@ -71,6 +82,12 @@ async function readSettingsRecordAsyncFromPath( return parseSettingsRecord(await fs.readFile(filePath, "utf8")); } +/** + * Best-effort backup reader for sync callers. + * + * Backup corruption is treated as an unavailable backup so callers can keep + * their legacy null-on-unavailable behavior. + */ function readSettingsBackupSync(): JsonRecord | null { try { return readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); @@ -79,6 +96,9 @@ function readSettingsBackupSync(): JsonRecord | null { } } +/** + * Best-effort backup reader for async callers. + */ async function readSettingsBackupAsync(): Promise { try { return await readSettingsRecordAsyncFromPath(UNIFIED_SETTINGS_BACKUP_PATH); @@ -87,7 +107,40 @@ async function readSettingsBackupAsync(): Promise { } } +/** + * Decide whether a primary settings read should fall back to the backup file. + * + * Missing primaries stay missing so callers do not merge stale backup state. + * Corrupt or unreadable primaries may fall back, while transient lock errors + * are rethrown so callers can retry. + */ +function shouldFallbackToSettingsBackup( + primaryExists: boolean, + error: unknown, +): boolean { + if (!primaryExists) { + return false; + } + const code = (error as NodeJS.ErrnoException | undefined)?.code; + if (code === "ENOENT") { + return false; + } + if (typeof code === "string" && TRANSIENT_READ_FS_CODES.has(code)) { + return false; + } + return true; +} + +/** + * Snapshot the primary settings file into `settings.json.bak` for sync writes. + * + * When the current in-memory state was recovered from backup, snapshotting is + * skipped so a corrupt primary cannot overwrite the only known-good backup. + */ function trySnapshotUnifiedSettingsBackupSync(): void { + if (backupDerivedReadPending) { + return; + } if (!existsSync(UNIFIED_SETTINGS_PATH)) { return; } @@ -104,7 +157,13 @@ function trySnapshotUnifiedSettingsBackupSync(): void { } } +/** + * Async variant of `trySnapshotUnifiedSettingsBackupSync`. + */ async function trySnapshotUnifiedSettingsBackupAsync(): Promise { + if (backupDerivedReadPending) { + return; + } if (!existsSync(UNIFIED_SETTINGS_PATH)) { return; } @@ -132,20 +191,25 @@ async function trySnapshotUnifiedSettingsBackupAsync(): Promise { * - Sensitive data: this function performs no token or secret redaction; any sensitive values present in the file are returned as-is and callers are responsible for redaction before logging or external exposure. */ function readSettingsRecordSync(): JsonRecord | null { + const primaryExists = existsSync(UNIFIED_SETTINGS_PATH); try { const primaryRecord = readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_PATH); if (primaryRecord) { return primaryRecord; } } catch (error) { + if (!shouldFallbackToSettingsBackup(primaryExists, error)) { + throw error; + } const backupRecord = readSettingsBackupSync(); if (backupRecord) { + backupDerivedReadPending = true; return backupRecord; } throw error; } - return readSettingsBackupSync(); + return null; } /** @@ -156,6 +220,7 @@ function readSettingsRecordSync(): JsonRecord | null { * @returns The parsed settings record as an object clone, or `null` if unavailable or invalid. */ async function readSettingsRecordAsync(): Promise { + const primaryExists = existsSync(UNIFIED_SETTINGS_PATH); try { const primaryRecord = await readSettingsRecordAsyncFromPath( UNIFIED_SETTINGS_PATH, @@ -164,14 +229,18 @@ async function readSettingsRecordAsync(): Promise { return primaryRecord; } } catch (error) { + if (!shouldFallbackToSettingsBackup(primaryExists, error)) { + throw error; + } const backupRecord = await readSettingsBackupAsync(); if (backupRecord) { + backupDerivedReadPending = true; return backupRecord; } throw error; } - return readSettingsBackupAsync(); + return null; } /** @@ -219,6 +288,7 @@ function writeSettingsRecordSync(record: JsonRecord): void { try { renameSync(tempPath, UNIFIED_SETTINGS_PATH); moved = true; + backupDerivedReadPending = false; return; } catch (error) { if (!isRetryableFsError(error) || attempt >= 4) { @@ -271,6 +341,7 @@ async function writeSettingsRecordAsync(record: JsonRecord): Promise { try { await fs.rename(tempPath, UNIFIED_SETTINGS_PATH); moved = true; + backupDerivedReadPending = false; return; } catch (error) { if (!isRetryableFsError(error) || attempt >= 4) { diff --git a/test/config-save.test.ts b/test/config-save.test.ts index b33ed7375..802e9839d 100644 --- a/test/config-save.test.ts +++ b/test/config-save.test.ts @@ -161,6 +161,35 @@ describe("plugin config save paths", () => { ).toBe(1); }); + it("does not warn for blank numeric env overrides", async () => { + process.env.CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY = ""; + vi.resetModules(); + const logWarnMock = vi.fn(); + + vi.doMock("../lib/logger.js", async () => { + const actual = + await vi.importActual( + "../lib/logger.js", + ); + return { + ...actual, + logWarn: logWarnMock, + }; + }); + + try { + const { getParallelProbingMaxConcurrency } = await import( + "../lib/config.js" + ); + expect( + getParallelProbingMaxConcurrency({ parallelProbingMaxConcurrency: 4 }), + ).toBe(4); + expect(logWarnMock).not.toHaveBeenCalled(); + } finally { + vi.doUnmock("../lib/logger.js"); + } + }); + it("normalizes fallback chain and drops invalid entries", async () => { const { getUnsupportedCodexFallbackChain } = await import("../lib/config.js"); diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts index d3d257000..d30c1f702 100644 --- a/test/plugin-config.test.ts +++ b/test/plugin-config.test.ts @@ -663,11 +663,11 @@ describe('Plugin Configuration', () => { it('should ignore invalid env values and fall back to config/default', () => { process.env.CODEX_MODE = 'maybe'; - const config: PluginConfig = { codexMode: true }; + const config: PluginConfig = { codexMode: false }; const result = getCodexMode(config); - expect(result).toBe(true); + expect(result).toBe(false); }); it('should use config codexMode=true when explicitly set', () => { @@ -1036,6 +1036,7 @@ describe('Plugin Configuration', () => { process.env.CODEX_AUTH_FETCH_TIMEOUT_MS = ''; expect(getFetchTimeoutMs({ fetchTimeoutMs: 90000 })).toBe(90000); delete process.env.CODEX_AUTH_FETCH_TIMEOUT_MS; + expect(getFetchTimeoutMs({})).toBe(60000); }); it('should read stream stall timeout from env', () => { diff --git a/test/storage-flagged.test.ts b/test/storage-flagged.test.ts index bfd4de24a..5a952fbfe 100644 --- a/test/storage-flagged.test.ts +++ b/test/storage-flagged.test.ts @@ -341,6 +341,43 @@ describe("flagged account storage", () => { readSpy.mockRestore(); }); + it("honors the reset marker even when it appears during backup recovery", async () => { + const backupPath = `${getFlaggedAccountsPath()}.bak`; + const resetMarkerPath = `${getFlaggedAccountsPath()}.reset-intent`; + await fs.writeFile( + backupPath, + JSON.stringify({ + version: 1, + accounts: [ + { + refreshToken: "backup-race", + flaggedAt: 1, + addedAt: 1, + lastUsed: 1, + }, + ], + }), + "utf8", + ); + + const originalReadFile = fs.readFile.bind(fs); + const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { + const [targetPath] = args; + const result = await originalReadFile(...args); + if (targetPath === backupPath) { + await fs.writeFile(resetMarkerPath, "reset", "utf8"); + } + return result; + }); + + try { + const flagged = await loadFlaggedAccounts(); + expect(flagged.accounts).toHaveLength(0); + } finally { + readSpy.mockRestore(); + } + }); + it("clears discovered flagged backup artifacts so manual snapshots cannot revive after clear", async () => { await saveFlaggedAccounts({ version: 1, diff --git a/test/storage-recovery-paths.test.ts b/test/storage-recovery-paths.test.ts index 0bbaad6d8..f93d9e29d 100644 --- a/test/storage-recovery-paths.test.ts +++ b/test/storage-recovery-paths.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeEach, afterEach } from "vitest"; +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; import { promises as fs, existsSync } from "node:fs"; import { createHash } from "node:crypto"; import { join } from "node:path"; @@ -159,6 +159,45 @@ describe("storage recovery paths", () => { expect(recovered.accounts[0]?.email).toBe("flagged2@example.com"); }); + it("suppresses flagged backup recovery when the reset marker appears mid-read", async () => { + const flaggedPath = join(workDir, "openai-codex-flagged-accounts.json"); + const backupPath = `${flaggedPath}.bak`; + const resetMarkerPath = `${flaggedPath}.reset-intent`; + await fs.writeFile( + backupPath, + JSON.stringify({ + version: 1, + accounts: [ + { + refreshToken: "flagged-race-refresh", + email: "race@example.com", + addedAt: 9, + lastUsed: 9, + flaggedAt: 9, + }, + ], + }), + "utf-8", + ); + + const originalReadFile = fs.readFile.bind(fs); + const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { + const [targetPath] = args; + const result = await originalReadFile(...args); + if (targetPath === backupPath) { + await fs.writeFile(resetMarkerPath, "reset", "utf-8"); + } + return result; + }); + + try { + const recovered = await loadFlaggedAccounts(); + expect(recovered.accounts).toHaveLength(0); + } finally { + readSpy.mockRestore(); + } + }); + it("falls back to historical backup snapshots when the latest backup is unreadable", async () => { await fs.writeFile(storagePath, "{broken-primary", "utf-8"); await fs.writeFile(`${storagePath}.bak`, "{broken-latest-backup", "utf-8"); diff --git a/test/unified-settings.test.ts b/test/unified-settings.test.ts index f29235ea1..852863e1c 100644 --- a/test/unified-settings.test.ts +++ b/test/unified-settings.test.ts @@ -85,6 +85,27 @@ describe("unified settings", () => { expect(await loadUnifiedDashboardSettings()).toBeNull(); }); + it("does not load backup settings when the primary file is missing", async () => { + const { + getUnifiedSettingsPath, + loadUnifiedPluginConfigSync, + loadUnifiedDashboardSettings, + } = await import("../lib/unified-settings.js"); + + await fs.writeFile( + `${getUnifiedSettingsPath()}.bak`, + JSON.stringify({ + version: 1, + pluginConfig: { codexMode: true }, + dashboardDisplaySettings: { uiThemePreset: "green" }, + }), + "utf8", + ); + + expect(loadUnifiedPluginConfigSync()).toBeNull(); + expect(await loadUnifiedDashboardSettings()).toBeNull(); + }); + it("recovers plugin config from backup when primary settings file is invalid", async () => { const { getUnifiedSettingsPath, @@ -125,6 +146,88 @@ describe("unified settings", () => { }); }); + it("preserves the last good backup when a write fails after a backup-derived read", async () => { + const { + getUnifiedSettingsPath, + loadUnifiedPluginConfigSync, + saveUnifiedPluginConfig, + } = await import("../lib/unified-settings.js"); + + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 45_000 }); + await saveUnifiedPluginConfig({ codexMode: false, fetchTimeoutMs: 90_000 }); + await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8"); + + expect(loadUnifiedPluginConfigSync()).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + + const backupPath = `${getUnifiedSettingsPath()}.bak`; + const copySpy = vi.spyOn(fs, "copyFile"); + const renameSpy = vi.spyOn(fs, "rename"); + renameSpy.mockImplementation(async () => { + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + }); + + try { + await expect( + saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 120_000 }), + ).rejects.toThrow(); + } finally { + copySpy.mockRestore(); + renameSpy.mockRestore(); + } + + expect(copySpy).not.toHaveBeenCalledWith( + getUnifiedSettingsPath(), + backupPath, + ); + const backupRecord = JSON.parse( + await fs.readFile(backupPath, "utf8"), + ) as { pluginConfig?: Record }; + expect(backupRecord.pluginConfig).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + }); + + it("resumes snapshotting after a backup-derived write succeeds", async () => { + const { + getUnifiedSettingsPath, + loadUnifiedPluginConfigSync, + saveUnifiedPluginConfig, + } = await import("../lib/unified-settings.js"); + + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 45_000 }); + await saveUnifiedPluginConfig({ codexMode: false, fetchTimeoutMs: 90_000 }); + await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8"); + + expect(loadUnifiedPluginConfigSync()).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 120_000 }); + let backupRecord = JSON.parse( + await fs.readFile(`${getUnifiedSettingsPath()}.bak`, "utf8"), + ) as { pluginConfig?: Record }; + expect(backupRecord.pluginConfig).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + + await saveUnifiedPluginConfig({ codexMode: false, fetchTimeoutMs: 150_000 }); + backupRecord = JSON.parse( + await fs.readFile(`${getUnifiedSettingsPath()}.bak`, "utf8"), + ) as { pluginConfig?: Record }; + expect(backupRecord.pluginConfig).toEqual({ + codexMode: true, + fetchTimeoutMs: 120_000, + }); + }); + it("returns null when dashboard settings file is missing", async () => { const { loadUnifiedDashboardSettings } = await import( "../lib/unified-settings.js" @@ -433,7 +536,7 @@ describe("unified settings", () => { }); }); - it("preserves settings sections when a primary read fails and backup recovery is available", async () => { + it("falls back to backup when the primary settings file is unreadable", async () => { const { saveUnifiedPluginConfig, saveUnifiedDashboardSettings, @@ -448,8 +551,8 @@ describe("unified settings", () => { const readSpy = vi.spyOn(fs, "readFile"); readSpy.mockImplementationOnce(async () => { - const error = new Error("file locked") as NodeJS.ErrnoException; - error.code = "EBUSY"; + const error = new Error("permission denied") as NodeJS.ErrnoException; + error.code = "EACCES"; throw error; }); @@ -476,4 +579,50 @@ describe("unified settings", () => { uiThemePreset: "yellow", }); }); + + it("rethrows transient primary read errors instead of falling back to backup", async () => { + const { + saveUnifiedPluginConfig, + saveUnifiedDashboardSettings, + getUnifiedSettingsPath, + } = await import("../lib/unified-settings.js"); + + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 70_000 }); + await saveUnifiedDashboardSettings({ + menuShowLastUsed: false, + uiThemePreset: "blue", + }); + + const readSpy = vi.spyOn(fs, "readFile"); + readSpy.mockImplementationOnce(async () => { + const error = new Error("file locked") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + }); + + try { + await expect( + saveUnifiedDashboardSettings({ + menuShowLastUsed: true, + uiThemePreset: "yellow", + }), + ).rejects.toThrow("file locked"); + } finally { + readSpy.mockRestore(); + } + + const raw = await fs.readFile(getUnifiedSettingsPath(), "utf8"); + const parsed = JSON.parse(raw) as { + pluginConfig?: Record; + dashboardDisplaySettings?: Record; + }; + expect(parsed.pluginConfig).toEqual({ + codexMode: true, + fetchTimeoutMs: 70_000, + }); + expect(parsed.dashboardDisplaySettings).toEqual({ + menuShowLastUsed: false, + uiThemePreset: "blue", + }); + }); }); From 4e25a4693589074bc2f8461ca6527187abc8f062 Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 12:47:22 +0800 Subject: [PATCH 13/18] fix: harden settings backup and env warnings --- lib/config.ts | 4 +- lib/unified-settings.ts | 91 +++++++++++++++++++++++++++------------- test/config-save.test.ts | 36 ++++++++++++++++ 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index fe87c22b8..e99a053d4 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -645,7 +645,7 @@ function resolveBooleanSetting( envValue === undefined ) { logConfigWarnOnce( - `Ignoring invalid boolean env ${envName}=${JSON.stringify(rawEnvValue)}. Expected 0/1, true/false, or yes/no.`, + `Ignoring invalid boolean env ${envName}. Expected 0/1, true/false, or yes/no.`, ); } if (envValue !== undefined) return envValue; @@ -677,7 +677,7 @@ function resolveNumberSetting( envValue === undefined ) { logConfigWarnOnce( - `Ignoring invalid numeric env ${envName}=${JSON.stringify(rawEnvValue)}. Expected a finite number.`, + `Ignoring invalid numeric env ${envName}. Expected a finite number.`, ); } const candidate = envValue ?? configValue ?? defaultValue; diff --git a/lib/unified-settings.ts b/lib/unified-settings.ts index 379621971..3444c01ee 100644 --- a/lib/unified-settings.ts +++ b/lib/unified-settings.ts @@ -21,7 +21,11 @@ const UNIFIED_SETTINGS_BACKUP_PATH = `${UNIFIED_SETTINGS_PATH}.bak`; const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]); const TRANSIENT_READ_FS_CODES = new Set(["EBUSY", "EAGAIN"]); let settingsWriteQueue: Promise = Promise.resolve(); -let backupDerivedReadPending = false; + +type SettingsReadResult = { + record: JsonRecord | null; + usedBackup: boolean; +}; function isRetryableFsError(error: unknown): boolean { const code = (error as NodeJS.ErrnoException | undefined)?.code; @@ -137,8 +141,10 @@ function shouldFallbackToSettingsBackup( * When the current in-memory state was recovered from backup, snapshotting is * skipped so a corrupt primary cannot overwrite the only known-good backup. */ -function trySnapshotUnifiedSettingsBackupSync(): void { - if (backupDerivedReadPending) { +function trySnapshotUnifiedSettingsBackupSync(options?: { + skipBecauseBackupRead?: boolean; +}): void { + if (options?.skipBecauseBackupRead) { return; } if (!existsSync(UNIFIED_SETTINGS_PATH)) { @@ -160,8 +166,10 @@ function trySnapshotUnifiedSettingsBackupSync(): void { /** * Async variant of `trySnapshotUnifiedSettingsBackupSync`. */ -async function trySnapshotUnifiedSettingsBackupAsync(): Promise { - if (backupDerivedReadPending) { +async function trySnapshotUnifiedSettingsBackupAsync(options?: { + skipBecauseBackupRead?: boolean; +}): Promise { + if (options?.skipBecauseBackupRead) { return; } if (!existsSync(UNIFIED_SETTINGS_PATH)) { @@ -190,12 +198,12 @@ async function trySnapshotUnifiedSettingsBackupAsync(): Promise { * - Windows: file locking on Windows may cause reads to fail; in those cases this function returns `null`. * - Sensitive data: this function performs no token or secret redaction; any sensitive values present in the file are returned as-is and callers are responsible for redaction before logging or external exposure. */ -function readSettingsRecordSync(): JsonRecord | null { +function readSettingsRecordSyncInternal(): SettingsReadResult { const primaryExists = existsSync(UNIFIED_SETTINGS_PATH); try { const primaryRecord = readSettingsRecordSyncFromPath(UNIFIED_SETTINGS_PATH); if (primaryRecord) { - return primaryRecord; + return { record: primaryRecord, usedBackup: false }; } } catch (error) { if (!shouldFallbackToSettingsBackup(primaryExists, error)) { @@ -203,13 +211,16 @@ function readSettingsRecordSync(): JsonRecord | null { } const backupRecord = readSettingsBackupSync(); if (backupRecord) { - backupDerivedReadPending = true; - return backupRecord; + return { record: backupRecord, usedBackup: true }; } throw error; } - return null; + return { record: null, usedBackup: false }; +} + +function readSettingsRecordSync(): JsonRecord | null { + return readSettingsRecordSyncInternal().record; } /** @@ -219,14 +230,14 @@ function readSettingsRecordSync(): JsonRecord | null { * * @returns The parsed settings record as an object clone, or `null` if unavailable or invalid. */ -async function readSettingsRecordAsync(): Promise { +async function readSettingsRecordAsyncInternal(): Promise { const primaryExists = existsSync(UNIFIED_SETTINGS_PATH); try { const primaryRecord = await readSettingsRecordAsyncFromPath( UNIFIED_SETTINGS_PATH, ); if (primaryRecord) { - return primaryRecord; + return { record: primaryRecord, usedBackup: false }; } } catch (error) { if (!shouldFallbackToSettingsBackup(primaryExists, error)) { @@ -234,13 +245,16 @@ async function readSettingsRecordAsync(): Promise { } const backupRecord = await readSettingsBackupAsync(); if (backupRecord) { - backupDerivedReadPending = true; - return backupRecord; + return { record: backupRecord, usedBackup: true }; } throw error; } - return null; + return { record: null, usedBackup: false }; +} + +async function readSettingsRecordAsync(): Promise { + return (await readSettingsRecordAsyncInternal()).record; } /** @@ -275,12 +289,17 @@ function normalizeForWrite(record: JsonRecord): JsonRecord { * * @param record - The settings object to persist; it will be normalized to include the unified settings version. */ -function writeSettingsRecordSync(record: JsonRecord): void { +function writeSettingsRecordSync( + record: JsonRecord, + options?: { skipBackupSnapshot?: boolean }, +): void { mkdirSync(getCodexMultiAuthDir(), { recursive: true }); const payload = normalizeForWrite(record); const data = `${JSON.stringify(payload, null, 2)}\n`; const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.tmp`; - trySnapshotUnifiedSettingsBackupSync(); + trySnapshotUnifiedSettingsBackupSync({ + skipBecauseBackupRead: options?.skipBackupSnapshot, + }); writeFileSync(tempPath, data, "utf8"); let moved = false; try { @@ -288,7 +307,6 @@ function writeSettingsRecordSync(record: JsonRecord): void { try { renameSync(tempPath, UNIFIED_SETTINGS_PATH); moved = true; - backupDerivedReadPending = false; return; } catch (error) { if (!isRetryableFsError(error) || attempt >= 4) { @@ -328,12 +346,17 @@ function writeSettingsRecordSync(record: JsonRecord): void { * * @param record - The settings object to persist; it will be normalized (version set) */ -async function writeSettingsRecordAsync(record: JsonRecord): Promise { +async function writeSettingsRecordAsync( + record: JsonRecord, + options?: { skipBackupSnapshot?: boolean }, +): Promise { await fs.mkdir(getCodexMultiAuthDir(), { recursive: true }); const payload = normalizeForWrite(record); const data = `${JSON.stringify(payload, null, 2)}\n`; const tempPath = `${UNIFIED_SETTINGS_PATH}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`; - await trySnapshotUnifiedSettingsBackupAsync(); + await trySnapshotUnifiedSettingsBackupAsync({ + skipBecauseBackupRead: options?.skipBackupSnapshot, + }); await fs.writeFile(tempPath, data, "utf8"); let moved = false; try { @@ -341,7 +364,6 @@ async function writeSettingsRecordAsync(record: JsonRecord): Promise { try { await fs.rename(tempPath, UNIFIED_SETTINGS_PATH); moved = true; - backupDerivedReadPending = false; return; } catch (error) { if (!isRetryableFsError(error) || attempt >= 4) { @@ -413,9 +435,12 @@ export function loadUnifiedPluginConfigSync(): JsonRecord | null { * @param pluginConfig - Key/value map representing plugin configuration to persist */ export function saveUnifiedPluginConfigSync(pluginConfig: JsonRecord): void { - const record = readSettingsRecordSync() ?? {}; - record.pluginConfig = { ...pluginConfig }; - writeSettingsRecordSync(record); + const { record, usedBackup } = readSettingsRecordSyncInternal(); + const nextRecord = record ?? {}; + nextRecord.pluginConfig = { ...pluginConfig }; + writeSettingsRecordSync(nextRecord, { + skipBackupSnapshot: usedBackup, + }); } /** @@ -430,9 +455,12 @@ export function saveUnifiedPluginConfigSync(pluginConfig: JsonRecord): void { */ export async function saveUnifiedPluginConfig(pluginConfig: JsonRecord): Promise { await enqueueSettingsWrite(async () => { - const record = await readSettingsRecordAsync() ?? {}; - record.pluginConfig = { ...pluginConfig }; - await writeSettingsRecordAsync(record); + const { record, usedBackup } = await readSettingsRecordAsyncInternal(); + const nextRecord = record ?? {}; + nextRecord.pluginConfig = { ...pluginConfig }; + await writeSettingsRecordAsync(nextRecord, { + skipBackupSnapshot: usedBackup, + }); }); } @@ -473,8 +501,11 @@ export async function saveUnifiedDashboardSettings( dashboardDisplaySettings: JsonRecord, ): Promise { await enqueueSettingsWrite(async () => { - const record = await readSettingsRecordAsync() ?? {}; - record.dashboardDisplaySettings = { ...dashboardDisplaySettings }; - await writeSettingsRecordAsync(record); + const { record, usedBackup } = await readSettingsRecordAsyncInternal(); + const nextRecord = record ?? {}; + nextRecord.dashboardDisplaySettings = { ...dashboardDisplaySettings }; + await writeSettingsRecordAsync(nextRecord, { + skipBackupSnapshot: usedBackup, + }); }); } diff --git a/test/config-save.test.ts b/test/config-save.test.ts index 802e9839d..9c233615d 100644 --- a/test/config-save.test.ts +++ b/test/config-save.test.ts @@ -190,6 +190,42 @@ describe("plugin config save paths", () => { } }); + it("redacts raw values from invalid numeric env override warnings", async () => { + process.env.CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY = "secret-value"; + vi.resetModules(); + const logWarnMock = vi.fn(); + + vi.doMock("../lib/logger.js", async () => { + const actual = + await vi.importActual( + "../lib/logger.js", + ); + return { + ...actual, + logWarn: logWarnMock, + }; + }); + + try { + const { getParallelProbingMaxConcurrency } = await import( + "../lib/config.js" + ); + expect( + getParallelProbingMaxConcurrency({ parallelProbingMaxConcurrency: 4 }), + ).toBe(4); + expect(logWarnMock).toHaveBeenCalledWith( + expect.stringContaining( + "Ignoring invalid numeric env CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY.", + ), + ); + expect(logWarnMock).not.toHaveBeenCalledWith( + expect.stringContaining("secret-value"), + ); + } finally { + vi.doUnmock("../lib/logger.js"); + } + }); + it("normalizes fallback chain and drops invalid entries", async () => { const { getUnsupportedCodexFallbackChain } = await import("../lib/config.js"); From e9d7d73e6d511cb41d6dca7eee1aebcdad09d93c Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 21:01:59 +0800 Subject: [PATCH 14/18] test: stabilize slow integration timeouts --- test/rotation-integration.test.ts | 18 +++++++++--------- test/settings-hub-utils.test.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/rotation-integration.test.ts b/test/rotation-integration.test.ts index a16fe37a0..b06efd2be 100644 --- a/test/rotation-integration.test.ts +++ b/test/rotation-integration.test.ts @@ -286,15 +286,15 @@ describe("Multi-Account Rotation Integration", () => { }); }); - describe("Storage mutex (file locking)", () => { - it("concurrent saves complete without corruption", async () => { - const storage = createStorageFromTestAccounts(TEST_ACCOUNTS.slice(0, 3)); - const manager = new AccountManager(undefined, storage); - - const saves = Array.from({ length: 10 }, () => manager.saveToDisk()); - await Promise.all(saves); - }); - }); + describe("Storage mutex (file locking)", () => { + it("concurrent saves complete without corruption", async () => { + const storage = createStorageFromTestAccounts(TEST_ACCOUNTS.slice(0, 3)); + const manager = new AccountManager(undefined, storage); + + const saves = Array.from({ length: 10 }, () => manager.saveToDisk()); + await Promise.all(saves); + }, 15_000); + }); describe("Debounced save", () => { it("saveToDiskDebounced does not throw", () => { diff --git a/test/settings-hub-utils.test.ts b/test/settings-hub-utils.test.ts index 36ca1e3bf..af3bc055a 100644 --- a/test/settings-hub-utils.test.ts +++ b/test/settings-hub-utils.test.ts @@ -232,7 +232,7 @@ describe("settings-hub utility coverage", () => { expect(() => api.clampBackendNumber("unknown-setting", 5)).toThrow( "Unknown backend numeric setting key", ); - }); + }, 15_000); it("formats layout mode labels", async () => { const api = await loadSettingsHubTestApi(); From 23c98bfd6cb37c3b568039e570f3f13c34010650 Mon Sep 17 00:00:00 2001 From: ndycode Date: Fri, 3 Apr 2026 21:55:37 +0800 Subject: [PATCH 15/18] fix: harden config saves and stabilize regressions --- lib/config.ts | 120 ++++++++++++++++-- lib/storage/flagged-storage-io.ts | 28 ++++- test/accounts.test.ts | 3 +- test/config-save.test.ts | 196 ++++++++++++++++++++++++++++-- test/rotation-integration.test.ts | 9 ++ test/storage-flagged.test.ts | 39 ------ 6 files changed, 330 insertions(+), 65 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index e99a053d4..4e0a676c2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -41,6 +41,13 @@ const UNSUPPORTED_CODEX_POLICIES = new Set(["strict", "fallback"]); const emittedConfigWarnings = new Set(); const configSaveQueues = new Map>(); const RETRYABLE_FS_CODES = new Set(["EBUSY", "EPERM"]); +const RETRYABLE_CONFIG_READ_CODES = new Set(["EBUSY", "EPERM", "EAGAIN"]); + +type ConfigReadState = + | { status: "missing" } + | { status: "ok"; record: Record } + | { status: "invalid"; errorMessage: string } + | { status: "unreadable"; errorMessage: string }; export type UnsupportedCodexPolicy = "strict" | "fallback"; @@ -456,6 +463,53 @@ function readConfigRecordFromPath( } } +async function readConfigRecordForSave( + configPath: string, +): Promise { + if (!existsSync(configPath)) { + return { status: "missing" }; + } + + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + const fileContent = await fs.readFile(configPath, "utf-8"); + const normalizedFileContent = stripUtf8Bom(fileContent); + const parsed = JSON.parse(normalizedFileContent) as unknown; + if (!isRecord(parsed)) { + const errorMessage = `Config at ${configPath} must contain a JSON object at the root.`; + logConfigWarnOnce(errorMessage); + return { status: "invalid", errorMessage }; + } + return { status: "ok", record: parsed }; + } catch (error) { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + if (code === "ENOENT") { + return { status: "missing" }; + } + if ( + typeof code === "string" && + RETRYABLE_CONFIG_READ_CODES.has(code) && + attempt < 4 + ) { + await sleep(10 * 2 ** attempt); + continue; + } + const errorMessage = `Failed to read config from ${configPath}: ${ + error instanceof Error ? error.message : String(error) + }`; + logConfigWarnOnce(errorMessage); + if (typeof code === "string" && RETRYABLE_CONFIG_READ_CODES.has(code)) { + return { status: "unreadable", errorMessage }; + } + return { status: "invalid", errorMessage }; + } + } + + const errorMessage = `Failed to read config from ${configPath}.`; + logConfigWarnOnce(errorMessage); + return { status: "unreadable", errorMessage }; +} + function resolveStoredPluginConfigRecord(): { configPath: string | null; storageKind: ConfigExplainStorageKind; @@ -507,10 +561,17 @@ function resolveStoredPluginConfigRecord(): { */ function sanitizePluginConfigForSave( config: Partial, -): Record { + ): { sanitized: Record; droppedKeys: string[] } { const normalized = sanitizePluginConfigRecord(config as Record); const entries = Object.entries((normalized ?? {}) as Record); const sanitized: Record = {}; + const inputRecord = isRecord(config) ? config : {}; + const droppedKeys = PLUGIN_CONFIG_KEYS.filter((key) => { + if (!(key in inputRecord) || inputRecord[key] === undefined) { + return false; + } + return !(key in (normalized ?? {})); + }); for (const [key, value] of entries) { if (value === undefined) continue; if (typeof value === "number" && !Number.isFinite(value)) continue; @@ -520,7 +581,7 @@ function sanitizePluginConfigForSave( } sanitized[key] = value; } - return sanitized; + return { sanitized, droppedKeys }; } /** @@ -539,14 +600,27 @@ function sanitizePluginConfigForSave( export async function savePluginConfig( configPatch: Partial, ): Promise { - const sanitizedPatch = sanitizePluginConfigForSave(configPatch); + const { sanitized: sanitizedPatch, droppedKeys } = + sanitizePluginConfigForSave(configPatch); + if (droppedKeys.length > 0) { + logConfigWarnOnce( + `Ignoring invalid plugin config field(s): ${droppedKeys.join(", ")}.`, + ); + } const envPath = (process.env.CODEX_MULTI_AUTH_CONFIG_PATH ?? "").trim(); if (envPath.length > 0) { await withConfigSaveLock(envPath, async () => { - const existingConfig = sanitizeStoredPluginConfigRecord( - readConfigRecordFromPath(envPath), - ); + const envConfigState = await readConfigRecordForSave(envPath); + if (envConfigState.status === "unreadable") { + throw new Error( + `Aborting config save because ${envPath} is unreadable.`, + ); + } + const existingConfig = + envConfigState.status === "ok" + ? sanitizeStoredPluginConfigRecord(envConfigState.record) + : null; const merged = { ...(existingConfig ?? {}), ...sanitizedPatch, @@ -558,14 +632,34 @@ export async function savePluginConfig( const unifiedPath = getUnifiedSettingsPath(); await withConfigSaveLock(unifiedPath, async () => { - const unifiedConfigRecord = loadUnifiedPluginConfigSync(); - const unifiedConfig = sanitizeStoredPluginConfigRecord( - unifiedConfigRecord, - ); - const legacyPath = unifiedConfigRecord ? null : resolvePluginConfigPath(); - const legacyConfig = legacyPath - ? sanitizeStoredPluginConfigRecord(readConfigRecordFromPath(legacyPath)) + const unifiedConfigState = await readConfigRecordForSave(unifiedPath); + if (unifiedConfigState.status === "unreadable") { + throw new Error( + `Aborting config save because ${unifiedPath} is unreadable.`, + ); + } + const unifiedConfigRecord = + unifiedConfigState.status === "ok" + ? unifiedConfigState.record.pluginConfig + : loadUnifiedPluginConfigSync(); + const unifiedConfig = sanitizeStoredPluginConfigRecord(unifiedConfigRecord); + const legacyPath = + unifiedConfigState.status === "missing" || + (unifiedConfigState.status === "ok" && !unifiedConfig) + ? resolvePluginConfigPath() + : null; + const legacyConfigState = legacyPath + ? await readConfigRecordForSave(legacyPath) : null; + if (legacyConfigState?.status === "unreadable") { + throw new Error( + `Aborting config save because ${legacyPath} is unreadable.`, + ); + } + const legacyConfig = + legacyConfigState?.status === "ok" + ? sanitizeStoredPluginConfigRecord(legacyConfigState.record) + : null; const merged = { ...(unifiedConfig ?? legacyConfig ?? diff --git a/lib/storage/flagged-storage-io.ts b/lib/storage/flagged-storage-io.ts index 068c3f06e..4c4837935 100644 --- a/lib/storage/flagged-storage-io.ts +++ b/lib/storage/flagged-storage-io.ts @@ -2,6 +2,8 @@ import { existsSync, promises as fs } from "node:fs"; import { dirname } from "node:path"; import type { FlaggedAccountStorageV1 } from "../storage.js"; +const RETRYABLE_UNLINK_CODES = new Set(["EBUSY", "EAGAIN", "EPERM"]); + /** * Return the ordered backup paths consulted for flagged-account recovery. */ @@ -9,6 +11,28 @@ function getFlaggedBackupPaths(path: string): string[] { return [`${path}.bak`, `${path}.bak.1`, `${path}.bak.2`]; } +async function unlinkWithRetry(candidatePath: string): Promise { + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + await fs.unlink(candidatePath); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === "ENOENT") { + return; + } + if ( + !code || + !RETRYABLE_UNLINK_CODES.has(code) || + attempt >= 4 + ) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt)); + } + } +} + export async function loadFlaggedAccountsState(params: { path: string; legacyPath: string; @@ -191,7 +215,7 @@ export async function clearFlaggedAccountsOnDisk(params: { ...params.backupPaths, ]) { try { - await fs.unlink(candidate); + await unlinkWithRetry(candidate); } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code !== "ENOENT") { @@ -208,7 +232,7 @@ export async function clearFlaggedAccountsOnDisk(params: { } if (!keepResetMarker) { try { - await fs.unlink(params.markerPath); + await unlinkWithRetry(params.markerPath); } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code !== "ENOENT") { diff --git a/test/accounts.test.ts b/test/accounts.test.ts index 93249b533..b5d354610 100644 --- a/test/accounts.test.ts +++ b/test/accounts.test.ts @@ -3221,6 +3221,7 @@ describe("AccountManager", () => { expires: now + 3600000, }); +<<<<<<< HEAD expect(account.accountId).toBe("account-enriched"); expect(account.email).toBe("enriched@example.com"); expect(getRuntimeAccountIdentityKey(account)).toBe( @@ -3229,7 +3230,7 @@ describe("AccountManager", () => { expect(getRuntimeTrackerKey(account)).toBe(trackerKey); expect(healthTracker.getScore(trackerKey, "codex:gpt-5.1")).toBeCloseTo( degradedScore, - 6, + 5, ); expect(tokenTracker.getTokens(trackerKey, "codex:gpt-5.1")).toBeLessThan(50); } finally { diff --git a/test/config-save.test.ts b/test/config-save.test.ts index 9c233615d..3b4222c98 100644 --- a/test/config-save.test.ts +++ b/test/config-save.test.ts @@ -126,21 +126,140 @@ describe("plugin config save paths", () => { it("writes through unified settings when env path is unset", async () => { delete process.env.CODEX_MULTI_AUTH_CONFIG_PATH; + const unifiedPath = join(tempDir, "settings.json"); + await fs.writeFile( + unifiedPath, + JSON.stringify({ + version: 1, + pluginConfig: { + preserved: 1, + codexMode: true, + }, + }), + "utf8", + ); - const { savePluginConfig, loadPluginConfig } = - await import("../lib/config.js"); - await savePluginConfig({ - codexMode: false, - parallelProbing: true, - parallelProbingMaxConcurrency: 7, + const logWarnMock = vi.fn(); + vi.doMock("../lib/logger.js", async () => { + const actual = + await vi.importActual( + "../lib/logger.js", + ); + return { + ...actual, + logWarn: logWarnMock, + }; }); - const loaded = loadPluginConfig(); - expect(loaded.codexMode).toBe(false); - expect(loaded.parallelProbing).toBe(true); - expect(loaded.parallelProbingMaxConcurrency).toBe(2); + try { + const { savePluginConfig, loadPluginConfig } = + await import("../lib/config.js"); + await savePluginConfig({ + codexMode: false, + parallelProbing: true, + parallelProbingMaxConcurrency: 7, + }); + + const loaded = loadPluginConfig(); + expect(loaded.codexMode).toBe(false); + expect(loaded.parallelProbing).toBe(true); + expect(loaded.parallelProbingMaxConcurrency).toBe(2); + + const parsed = JSON.parse(await fs.readFile(unifiedPath, "utf8")) as { + pluginConfig?: Record; + }; + expect(parsed.pluginConfig).toEqual({ + preserved: 1, + codexMode: false, + parallelProbing: true, + }); + expect(logWarnMock).toHaveBeenCalledWith( + expect.stringContaining( + "Ignoring invalid plugin config field(s): parallelProbingMaxConcurrency.", + ), + ); + } finally { + vi.doUnmock("../lib/logger.js"); + } }); + it("does not overwrite an unreadable env-path config file", async () => { + const configPath = join(tempDir, "plugin-config.json"); + process.env.CODEX_MULTI_AUTH_CONFIG_PATH = configPath; + await fs.writeFile( + configPath, + JSON.stringify({ codexMode: true, preserved: 1 }), + "utf8", + ); + + const originalReadFile = fs.readFile.bind(fs); + const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { + const [targetPath] = args; + if (targetPath === configPath) { + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + } + return originalReadFile(...args); + }); + + try { + const { savePluginConfig } = await import("../lib/config.js"); + await expect(savePluginConfig({ codexMode: false })).rejects.toThrow( + "unreadable", + ); + } finally { + readSpy.mockRestore(); + } + + const parsed = JSON.parse(await fs.readFile(configPath, "utf8")) as Record< + string, + unknown + >; + expect(parsed).toEqual({ codexMode: true, preserved: 1 }); + }); + + it("does not overwrite an unreadable unified settings file", async () => { + delete process.env.CODEX_MULTI_AUTH_CONFIG_PATH; + const unifiedPath = join(tempDir, "settings.json"); + await fs.writeFile( + unifiedPath, + JSON.stringify({ + version: 1, + pluginConfig: { codexMode: true, preserved: 1 }, + dashboardDisplaySettings: { uiThemePreset: "green" }, + }), + "utf8", + ); + + const originalReadFile = fs.readFile.bind(fs); + const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { + const [targetPath] = args; + if (targetPath === unifiedPath) { + const error = new Error("busy") as NodeJS.ErrnoException; + error.code = "EBUSY"; + throw error; + } + return originalReadFile(...args); + }); + + try { + const { savePluginConfig } = await import("../lib/config.js"); + await expect(savePluginConfig({ codexMode: false })).rejects.toThrow( + "unreadable", + ); + } finally { + readSpy.mockRestore(); + } + + const parsed = JSON.parse(await fs.readFile(unifiedPath, "utf8")) as { + pluginConfig?: Record; + dashboardDisplaySettings?: Record; + }; + expect(parsed.pluginConfig).toEqual({ codexMode: true, preserved: 1 }); + expect(parsed.dashboardDisplaySettings).toEqual({ uiThemePreset: "green" }); + }); + it("resolves parallel probing settings and clamps concurrency", async () => { const { getParallelProbing, getParallelProbingMaxConcurrency } = await import("../lib/config.js"); @@ -190,6 +309,63 @@ describe("plugin config save paths", () => { } }); + it("does not warn for blank boolean env overrides", async () => { + process.env.CODEX_AUTH_PARALLEL_PROBING = ""; + vi.resetModules(); + const logWarnMock = vi.fn(); + + vi.doMock("../lib/logger.js", async () => { + const actual = + await vi.importActual( + "../lib/logger.js", + ); + return { + ...actual, + logWarn: logWarnMock, + }; + }); + + try { + const { getParallelProbing } = await import("../lib/config.js"); + expect(getParallelProbing({ parallelProbing: false })).toBe(false); + expect(logWarnMock).not.toHaveBeenCalled(); + } finally { + vi.doUnmock("../lib/logger.js"); + } + }); + + it("redacts raw values from invalid boolean env override warnings", async () => { + process.env.CODEX_AUTH_PARALLEL_PROBING = "secret-bool"; + vi.resetModules(); + const logWarnMock = vi.fn(); + + vi.doMock("../lib/logger.js", async () => { + const actual = + await vi.importActual( + "../lib/logger.js", + ); + return { + ...actual, + logWarn: logWarnMock, + }; + }); + + try { + const { getParallelProbing } = await import("../lib/config.js"); + expect(getParallelProbing({ parallelProbing: false })).toBe(false); + expect(logWarnMock).toHaveBeenCalledWith( + expect.stringContaining( + "Ignoring invalid boolean env CODEX_AUTH_PARALLEL_PROBING.", + ), + ); + expect(logWarnMock).not.toHaveBeenCalledWith( + expect.stringContaining("secret-bool"), + ); + } finally { + vi.doUnmock("../lib/logger.js"); + } + }); + it("redacts raw values from invalid numeric env override warnings", async () => { process.env.CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY = "secret-value"; vi.resetModules(); diff --git a/test/rotation-integration.test.ts b/test/rotation-integration.test.ts index b06efd2be..4f6716ac3 100644 --- a/test/rotation-integration.test.ts +++ b/test/rotation-integration.test.ts @@ -293,6 +293,15 @@ describe("Multi-Account Rotation Integration", () => { const saves = Array.from({ length: 10 }, () => manager.saveToDisk()); await Promise.all(saves); + + const parsed = JSON.parse( + await fs.readFile(testStoragePath, "utf8"), + ) as AccountStorageV3; + expect(parsed.version).toBe(3); + expect(parsed.accounts).toHaveLength(3); + expect(parsed.accounts.map((account) => account.email)).toEqual( + TEST_ACCOUNTS.slice(0, 3).map((account) => account.email), + ); }, 15_000); }); diff --git a/test/storage-flagged.test.ts b/test/storage-flagged.test.ts index 5a952fbfe..2e624e222 100644 --- a/test/storage-flagged.test.ts +++ b/test/storage-flagged.test.ts @@ -451,45 +451,6 @@ describe("flagged account storage", () => { unlinkSpy.mockRestore(); }); - it("suppresses flagged accounts when clear cannot delete the primary file after writing the reset marker", async () => { - await saveFlaggedAccounts({ - version: 1, - accounts: [ - { - refreshToken: "stale-primary", - flaggedAt: 1, - addedAt: 1, - lastUsed: 1, - }, - ], - }); - - const flaggedPath = getFlaggedAccountsPath(); - const originalUnlink = fs.unlink.bind(fs); - const unlinkSpy = vi - .spyOn(fs, "unlink") - .mockImplementation(async (targetPath) => { - if (targetPath === flaggedPath) { - const error = new Error( - "EPERM primary delete", - ) as NodeJS.ErrnoException; - error.code = "EPERM"; - throw error; - } - return originalUnlink(targetPath); - }); - - await expect(clearFlaggedAccounts()).rejects.toThrow( - "EPERM primary delete", - ); - - const flagged = await loadFlaggedAccounts(); - expect(existsSync(flaggedPath)).toBe(true); - expect(flagged.accounts).toHaveLength(0); - - unlinkSpy.mockRestore(); - }); - it("emits snapshot metadata for flagged account backups", async () => { await saveFlaggedAccounts({ version: 1, From a7728502ef9fc070b77d6beb38867dd1f5043101 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 5 Apr 2026 10:42:34 +0800 Subject: [PATCH 16/18] release: rebuild main PR wave for v1.2.3 --- README.md | 6 +- docs/README.md | 9 +- docs/releases/v1.2.3.md | 53 +++++++++++ index.ts | 5 + lib/preemptive-quota-scheduler.ts | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/codex.js | 93 +++++++++++++----- test/accounts.test.ts | 1 - test/codex-bin-wrapper.test.ts | 119 ++++++++++++++++++------ test/documentation.test.ts | 4 +- test/fetch-helpers.test.ts | 16 ++-- test/index.test.ts | 13 ++- test/preemptive-quota-scheduler.test.ts | 20 ++++ 14 files changed, 275 insertions(+), 72 deletions(-) create mode 100644 docs/releases/v1.2.3.md diff --git a/README.md b/README.md index 0e1328e8f..cc710369a 100644 --- a/README.md +++ b/README.md @@ -308,9 +308,9 @@ codex auth doctor --json ## Release Notes -- Current stable: [docs/releases/v1.2.2.md](docs/releases/v1.2.2.md) -- Previous stable: [docs/releases/v1.2.1.md](docs/releases/v1.2.1.md) -- Earlier stable: [docs/releases/v1.2.0.md](docs/releases/v1.2.0.md) +- Current stable: [docs/releases/v1.2.3.md](docs/releases/v1.2.3.md) +- Previous stable: [docs/releases/v1.2.2.md](docs/releases/v1.2.2.md) +- Earlier stable: [docs/releases/v1.2.1.md](docs/releases/v1.2.1.md) - Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md) ## License diff --git a/docs/README.md b/docs/README.md index 6f8098963..6ff444e7c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,9 +23,10 @@ Public documentation for `codex-multi-auth`. | [configuration.md](configuration.md) | Stable defaults, precedence, and environment overrides | | [architecture.md](architecture.md) | Public system overview of the wrapper, storage, and optional plugin runtime | | [privacy.md](privacy.md) | Data handling and local storage behavior | -| [releases/v1.2.2.md](releases/v1.2.2.md) | Stable release notes | -| [releases/v1.2.1.md](releases/v1.2.1.md) | Previous stable release notes | -| [releases/v1.2.0.md](releases/v1.2.0.md) | Earlier stable release notes | +| [releases/v1.2.3.md](releases/v1.2.3.md) | Stable release notes | +| [releases/v1.2.2.md](releases/v1.2.2.md) | Previous stable release notes | +| [releases/v1.2.1.md](releases/v1.2.1.md) | Earlier stable release notes | +| [releases/v1.2.0.md](releases/v1.2.0.md) | Archived stable release notes | | [releases/v0.1.7.md](releases/v0.1.7.md) | Archived stable release notes | | [releases/v0.1.6.md](releases/v0.1.6.md) | Archived stable release notes | | [releases/v0.1.5.md](releases/v0.1.5.md) | Archived stable release notes | @@ -51,7 +52,7 @@ Public documentation for `codex-multi-auth`. | [reference/storage-paths.md](reference/storage-paths.md) | Canonical and compatibility storage paths | | [reference/public-api.md](reference/public-api.md) | Public API stability and semver contract | | [reference/error-contracts.md](reference/error-contracts.md) | CLI, JSON, and helper error semantics | -| [releases/v1.2.2.md](releases/v1.2.2.md) | Current stable release notes | +| [releases/v1.2.3.md](releases/v1.2.3.md) | Current stable release notes | | [releases/v0.1.0-beta.0.md](releases/v0.1.0-beta.0.md) | Archived prerelease reference | | [Daily Use release notes](#daily-use) | Stable, previous, and archived release notes | | [releases/legacy-pre-0.1-history.md](releases/legacy-pre-0.1-history.md) | Archived pre-0.1 changelog history | diff --git a/docs/releases/v1.2.3.md b/docs/releases/v1.2.3.md new file mode 100644 index 000000000..581235f07 --- /dev/null +++ b/docs/releases/v1.2.3.md @@ -0,0 +1,53 @@ +# Release v1.2.3 + +Release line: `stable` + +This release supersedes the open `main`-target PR wave with one rebuilt, validated integration branch. + +## Scope + +- Current package version in `package.json` is `1.2.3`. +- Canonical command family remains `codex auth ...`. +- Canonical package name remains `codex-multi-auth`. +- The release branch rebuild starts from `origin/main` commit `cbce5f5c3c5588c08a388d600306366ccb95a6a7`. + +## What Changed + +- remediated the `audit:ci` dependency findings and refreshed the lockfile on current `main` +- fixed ready-first account ordering, including the menu auto-refresh race that could re-skip a later refresh +- hardened the Codex wrapper compatibility path so unsupported reasoning-effort config is rewritten correctly, staged auth state syncs back before cleanup, and cleanup-failure tests do not rely on source rewriting +- fixed usage-limit cooldown persistence across account state, fallback 429 handling, and quota scheduling without dropping secondary quota state +- carried forward the config validation, unified-settings backup recovery, and flagged-backup recovery hardening from the older config/storage PR lane + +## Included PR Lanes + +- `#351` `chore: remediate audit-ci dependency findings` +- `#352` `fix ready-first account ordering regressions` +- `#353` `fix codex wrapper compatibility handling` +- `#354` `fix usage-limit cooldown persistence` +- `#344` `fix config validation and flagged backup recovery` + +## Superseded PR Stack + +- `#344` `fix config validation and flagged backup recovery` +- `#351` `chore: remediate audit-ci dependency findings` +- `#352` `fix ready-first account ordering regressions` +- `#353` `fix codex wrapper compatibility handling` +- `#354` `fix usage-limit cooldown persistence` + +## Validation + +- `npm run lint` +- `npm run typecheck` +- `npm test -- --pool=threads --maxWorkers=1` +- `npm run build` +- `npm run clean:repo:check` +- `npm run audit:ci` +- Full suite passed: `222/222` files, `3292/3292` tests + +## Related + +- [../getting-started.md](../getting-started.md) +- [../upgrade.md](../upgrade.md) +- [../reference/commands.md](../reference/commands.md) +- [../reference/public-api.md](../reference/public-api.md) diff --git a/index.ts b/index.ts index df01ed8c3..c3a0be049 100644 --- a/index.ts +++ b/index.ts @@ -2148,6 +2148,11 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { if (!fallbackResponse.ok) { const { response: handledFallbackResponse, rateLimit: fallbackRateLimit } = await handleErrorResponse(fallbackResponse); + try { + await fallbackResponse.body?.cancel(); + } catch { + // Best-effort only; the error body has already been read. + } if (handledFallbackResponse.status === 429) { const retryAfterMs = fallbackRateLimit?.retryAfterMs ?? 60_000; diff --git a/lib/preemptive-quota-scheduler.ts b/lib/preemptive-quota-scheduler.ts index 4d8c22d07..605c48318 100644 --- a/lib/preemptive-quota-scheduler.ts +++ b/lib/preemptive-quota-scheduler.ts @@ -224,7 +224,7 @@ export class PreemptiveQuotaScheduler { usedPercent: 100, resetAtMs: nextResetAtMs, }, - secondary: {}, + secondary: existing?.secondary ? { ...existing.secondary } : {}, updatedAt: Math.max(existing?.updatedAt ?? 0, now), }); } diff --git a/package-lock.json b/package-lock.json index de0cb0a52..a5479cb1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-multi-auth", - "version": "1.2.2", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-multi-auth", - "version": "1.2.2", + "version": "1.2.3", "bundleDependencies": [ "@codex-ai/plugin" ], diff --git a/package.json b/package.json index aaeba506a..e658aca9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codex-multi-auth", - "version": "1.2.2", + "version": "1.2.3", "description": "Multi-account OAuth manager and codex auth wrapper for the official @openai/codex CLI, with switching, health checks, and recovery tools", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/scripts/codex.js b/scripts/codex.js index e4ca8e133..63eac3b20 100755 --- a/scripts/codex.js +++ b/scripts/codex.js @@ -9,6 +9,7 @@ import { mkdtempSync, readFileSync, rmSync, + statSync, writeFileSync, } from "node:fs"; import { createRequire } from "node:module"; @@ -21,6 +22,11 @@ import { normalizeAuthAlias, shouldHandleMultiAuthAuth } from "./codex-routing.j const RETRYABLE_SHADOW_HOME_CLEANUP_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]); const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120]; +const SHADOW_HOME_STATE_FILES = ["auth.json", "accounts.json", ".codex-global-state.json"]; +let shadowHomeCleanupBusyFailuresRemaining = Number.parseInt( + process.env.CODEX_MULTI_AUTH_TEST_SHADOW_CLEANUP_BUSY_FAILURES ?? "0", + 10, +); function isRetryableShadowHomeCleanupError(error) { const code = error && typeof error === "object" && "code" in error ? error.code : undefined; @@ -37,6 +43,12 @@ function sleepSync(ms) { function removeDirectoryWithRetry(targetPath) { for (let attempt = 0; attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length; attempt += 1) { try { + if (shadowHomeCleanupBusyFailuresRemaining > 0) { + shadowHomeCleanupBusyFailuresRemaining -= 1; + const error = new Error("simulated busy cleanup"); + error.code = "EBUSY"; + throw error; + } rmSync(targetPath, { recursive: true, force: true }); return; } catch (error) { @@ -477,21 +489,19 @@ function resolveOriginalMultiAuthDir(env) { return undefined; } -function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { - const { args: nextArgs, requestedModel } = rewriteReasoningConfigArgs(rawArgs); +function createCompatibilityCodexHome( + processedArgs, + requestedModel, + baseEnv = process.env, +) { if (!requestedModel) { - return { args: nextArgs, env: baseEnv, cleanup: undefined }; - } - - const xhighCompatEffort = coerceReasoningEffortForModel(requestedModel, "xhigh"); - if (xhighCompatEffort === "xhigh") { - return { args: nextArgs, env: baseEnv, cleanup: undefined }; + return { args: processedArgs, env: baseEnv, cleanup: undefined }; } const originalCodexHome = resolveCodexHomeDir(baseEnv); const configPath = join(originalCodexHome, "config.toml"); if (!existsSync(configPath)) { - return { args: nextArgs, env: baseEnv, cleanup: undefined }; + return { args: processedArgs, env: baseEnv, cleanup: undefined }; } const rawConfig = readFileSync(configPath, "utf8"); @@ -500,7 +510,7 @@ function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { requestedModel, ); if (compatConfig === rawConfig) { - return { args: nextArgs, env: baseEnv, cleanup: undefined }; + return { args: processedArgs, env: baseEnv, cleanup: undefined }; } const shadowCodexHome = mkdtempSync(join(tmpdir(), "codex-multi-auth-home-")); @@ -518,11 +528,34 @@ function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { // Best-effort only; permission semantics vary by platform. } }; + const syncShadowHomeStateBack = () => { + for (const name of SHADOW_HOME_STATE_FILES) { + const shadowPath = join(shadowCodexHome, name); + if (!existsSync(shadowPath)) { + continue; + } + + try { + const shadowStats = statSync(shadowPath); + const originalPath = join(originalCodexHome, name); + if (existsSync(originalPath)) { + const originalStats = statSync(originalPath); + if (originalStats.isFile() && originalStats.mtimeMs > shadowStats.mtimeMs) { + continue; + } + } + copyFileSync(shadowPath, originalPath); + tightenShadowHomePermissions(originalPath); + } catch { + // Best-effort only; runtime auth refreshes should not fail cleanup. + } + } + }; try { const compatConfigPath = join(shadowCodexHome, "config.toml"); writeFileSync(compatConfigPath, compatConfig, "utf8"); tightenShadowHomePermissions(compatConfigPath); - for (const name of ["auth.json", "accounts.json", ".codex-global-state.json"]) { + for (const name of SHADOW_HOME_STATE_FILES) { const sourcePath = join(originalCodexHome, name); if (existsSync(sourcePath)) { const destinationPath = join(shadowCodexHome, name); @@ -534,6 +567,10 @@ function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { cleanup(); throw error; } + const cleanupWithSync = () => { + syncShadowHomeStateBack(); + cleanup(); + }; const forwardedEnv = { ...baseEnv, @@ -545,23 +582,30 @@ function createCompatibilityCodexHome(rawArgs, baseEnv = process.env) { } return { - args: nextArgs, + args: processedArgs, env: forwardedEnv, - cleanup, + cleanup: cleanupWithSync, }; } function buildForwardArgs(rawArgs) { - const { args: compatibilityArgs } = rewriteReasoningConfigArgs(rawArgs); + const { args: compatibilityArgs, requestedModel } = rewriteReasoningConfigArgs(rawArgs); const forceFileAuthStore = (process.env.CODEX_MULTI_AUTH_FORCE_FILE_AUTH_STORE ?? "1").trim() !== "0"; - if (!forceFileAuthStore) return compatibilityArgs; - if (hasCliAuthCredentialsStoreOverride(compatibilityArgs)) return compatibilityArgs; + if (!forceFileAuthStore) { + return { args: compatibilityArgs, requestedModel }; + } + if (hasCliAuthCredentialsStoreOverride(compatibilityArgs)) { + return { args: compatibilityArgs, requestedModel }; + } - return [ - ...compatibilityArgs, - "-c", - 'cli_auth_credentials_store="file"', - ]; + return { + args: [ + ...compatibilityArgs, + "-c", + 'cli_auth_credentials_store="file"', + ], + requestedModel, + }; } function normalizeExitCode(value) { @@ -919,8 +963,11 @@ async function main() { } await autoSyncManagerActiveSelectionIfEnabled(); - const forwardArgs = buildForwardArgs(rawArgs); - const compatibility = createCompatibilityCodexHome(forwardArgs); + const { args: forwardArgs, requestedModel } = buildForwardArgs(rawArgs); + const compatibility = createCompatibilityCodexHome( + forwardArgs, + requestedModel, + ); return forwardToRealCodex( realCodexBin, compatibility.args, diff --git a/test/accounts.test.ts b/test/accounts.test.ts index b5d354610..9b2367d17 100644 --- a/test/accounts.test.ts +++ b/test/accounts.test.ts @@ -3221,7 +3221,6 @@ describe("AccountManager", () => { expires: now + 3600000, }); -<<<<<<< HEAD expect(account.accountId).toBe("account-enriched"); expect(account.email).toBe("enriched@example.com"); expect(getRuntimeAccountIdentityKey(account)).toBe( diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index a1f902aa2..56685e6b9 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -87,32 +87,11 @@ function createCustomFakeCodexBin(rootDir: string, lines: string[]): string { } function injectShadowCleanupBusyFailures( - fixtureRoot: string, failuresBeforeSuccess = 2, -): void { - const wrapperPath = join(fixtureRoot, "scripts", "codex.js"); - const originalSource = readFileSync(wrapperPath, "utf8"); - const instrumentedSource = originalSource - .replace( - 'const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120];', - [ - 'const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120];', - `globalThis.__cleanupBusyFailuresRemaining = ${failuresBeforeSuccess};`, - ].join("\n"), - ) - .replace( - "rmSync(targetPath, { recursive: true, force: true });", - [ - "if (globalThis.__cleanupBusyFailuresRemaining > 0) {", - "\tglobalThis.__cleanupBusyFailuresRemaining -= 1;", - '\tconst error = new Error("simulated busy cleanup");', - '\terror.code = "EBUSY";', - "\tthrow error;", - "}", - "rmSync(targetPath, { recursive: true, force: true });", - ].join("\n"), - ); - writeFileSync(wrapperPath, instrumentedSource, "utf8"); +): NodeJS.ProcessEnv { + return { + CODEX_MULTI_AUTH_TEST_SHADOW_CLEANUP_BUSY_FAILURES: String(failuresBeforeSuccess), + }; } function createFakeGlobalCodexInstall(rootDir: string): string { @@ -398,7 +377,7 @@ describe("codex bin wrapper", () => { it("cleans up compatibility shadow homes when staging fails", () => { const fixtureRoot = createWrapperFixture(); - injectShadowCleanupBusyFailures(fixtureRoot); + const cleanupFailureEnv = injectShadowCleanupBusyFailures(); const fakeBin = createFakeCodexBin(fixtureRoot); const originalHome = join(fixtureRoot, "codex-home"); const controlledTmp = join(fixtureRoot, "tmp"); @@ -421,6 +400,7 @@ describe("codex bin wrapper", () => { TMP: controlledTmp, TEMP: controlledTmp, TMPDIR: controlledTmp, + ...cleanupFailureEnv, }, ); @@ -432,6 +412,51 @@ describe("codex bin wrapper", () => { ).toEqual([]); }); + it("syncs refreshed auth state back from compatibility shadow homes before cleanup", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ + "#!/usr/bin/env node", + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'const home = process.env.CODEX_HOME ?? "";', + 'fs.writeFileSync(path.join(home, "auth.json"), \'{"token":"shadow"}\\n\', "utf8");', + 'fs.writeFileSync(path.join(home, "accounts.json"), \'{"accounts":["shadow"]}\\n\', "utf8");', + 'fs.writeFileSync(path.join(home, ".codex-global-state.json"), \'{"last":"shadow"}\\n\', "utf8");', + 'console.log(`CODEX_HOME:${home}`);', + "process.exit(0);", + ]); + const originalHome = join(fixtureRoot, "codex-home"); + const controlledTmp = join(fixtureRoot, "tmp"); + mkdirSync(originalHome, { recursive: true }); + mkdirSync(controlledTmp, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), '{"token":"original"}\n', "utf8"); + writeFileSync(join(originalHome, "accounts.json"), '{"accounts":["original"]}\n', "utf8"); + writeFileSync(join(originalHome, ".codex-global-state.json"), '{"last":"original"}\n', "utf8"); + writeFileSync(join(originalHome, "config.toml"), 'model_reasoning_effort = "xhigh"\n', "utf8"); + + const result = runWrapper( + fixtureRoot, + ["exec", "status", "--model", "gpt-5.1"], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + TMP: controlledTmp, + TEMP: controlledTmp, + TMPDIR: controlledTmp, + }, + ); + + expect(result.status).toBe(0); + expect(readFileSync(join(originalHome, "auth.json"), "utf8").trim()).toBe('{"token":"shadow"}'); + expect(readFileSync(join(originalHome, "accounts.json"), "utf8").trim()).toBe('{"accounts":["shadow"]}'); + expect(readFileSync(join(originalHome, ".codex-global-state.json"), "utf8").trim()).toBe('{"last":"shadow"}'); + expect( + readdirSync(controlledTmp).filter((entry) => + entry.startsWith("codex-multi-auth-home-"), + ), + ).toEqual([]); + }); + it("rewrites unquoted config reasoning effort values for mini compatibility models", () => { const fixtureRoot = createWrapperFixture(); const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ @@ -572,6 +597,48 @@ describe("codex bin wrapper", () => { ); }); + it("rewrites config reasoning effort when the model supports xhigh but rejects none", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ + "#!/usr/bin/env node", + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'console.log(`CODEX_HOME:${process.env.CODEX_HOME ?? ""}`);', + 'const configPath = path.join(process.env.CODEX_HOME ?? "", "config.toml");', + 'console.log(fs.readFileSync(configPath, "utf8").trim());', + "process.exit(0);", + ]); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync( + join(originalHome, "config.toml"), + [ + 'model_reasoning_effort = "none"', + 'profile = "legacy-pro"', + "", + '[profiles."legacy-pro"]', + 'model_reasoning_effort = "none"', + "", + ].join("\n"), + "utf8", + ); + + const result = runWrapper( + fixtureRoot, + ["exec", "status", "--model", "gpt-5.4-pro"], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).not.toContain(`CODEX_HOME:${originalHome}`); + expect(result.stdout).toContain('model_reasoning_effort = "medium"'); + expect(result.stdout).not.toContain('model_reasoning_effort = "none"'); + }); + it.skipIf(process.platform !== "win32")( "installs Windows codex shell guards to survive shim takeover", () => { diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 99f75c502..b6a550879 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -34,8 +34,8 @@ function readPackageVersion(): string { let packageVersion = ""; let currentStableReleaseDoc = ""; // These stay manual so the docs portal keeps an intentional short stable-history window. -const previousStableReleaseDoc = "docs/releases/v1.2.1.md"; -const earlierStableReleaseDoc = "docs/releases/v1.2.0.md"; +const previousStableReleaseDoc = "docs/releases/v1.2.2.md"; +const earlierStableReleaseDoc = "docs/releases/v1.2.1.md"; function getUserDocs(): string[] { return [ diff --git a/test/fetch-helpers.test.ts b/test/fetch-helpers.test.ts index a79b51514..3cfb1581e 100644 --- a/test/fetch-helpers.test.ts +++ b/test/fetch-helpers.test.ts @@ -1178,14 +1178,14 @@ describe('createEntitlementErrorResponse', () => { } }); - it('caps retryAfterMs at 7 days', async () => { - const body = { error: { message: 'rate limited', retry_after_ms: 10 * 24 * 60 * 60 * 1000 } }; - const response = new Response(JSON.stringify(body), { status: 429 }); - - const { rateLimit } = await handleErrorResponse(response); - - expect(rateLimit?.retryAfterMs).toBe(7 * 24 * 60 * 60 * 1000); - }); + it('caps retryAfterMs at 7 days', async () => { + const body = { error: { message: 'rate limited', retry_after_ms: 10 * 24 * 60 * 60 * 1000 } }; + const response = new Response(JSON.stringify(body), { status: 429 }); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit?.retryAfterMs).toBe(7 * 24 * 60 * 60 * 1000); + }); it('caps retry-after-ms header values at 7 days', async () => { const headers = new Headers({ diff --git a/test/index.test.ts b/test/index.test.ts index cbc71ec2f..8678d8996 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4445,6 +4445,16 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { const markRateLimitedWithReason = vi.fn(); const recordRateLimit = vi.fn(); const pendingFailovers: Array> = []; + const fallback429Response = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("rate limited")); + controller.close(); + }, + }), + { status: 429 }, + ); + const fallbackCancelSpy = vi.spyOn(fallback429Response.body!, "cancel"); const manager = { getAccountCount: () => 2, getCurrentOrNextForFamilyHybrid: () => currentAccount, @@ -4525,7 +4535,7 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { if (fetchCount === 1) { return new Response("data: ok\n\n", { status: 200 }); } - return new Response("rate limited", { status: 429 }); + return fallback429Response; }); const mockClient = createMockClient(); @@ -4545,6 +4555,7 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { "gpt-5.1", "gpt-5.1", ); + expect(fallbackCancelSpy).toHaveBeenCalledTimes(1); }); it("forwards persistence error toast arguments through manual OAuth flow", async () => { diff --git a/test/preemptive-quota-scheduler.test.ts b/test/preemptive-quota-scheduler.test.ts index 3a0513e48..cb6bb61c7 100644 --- a/test/preemptive-quota-scheduler.test.ts +++ b/test/preemptive-quota-scheduler.test.ts @@ -87,6 +87,26 @@ describe("preemptive quota scheduler", () => { expect(decision.waitMs).toBe(25_000); }); + it("preserves secondary near-exhaustion state when marking a quota key rate-limited", () => { + const scheduler = new PreemptiveQuotaScheduler({ + remainingPercentThresholdSecondary: 5, + }); + scheduler.update("acc:model", { + status: 200, + primary: { usedPercent: 10, resetAtMs: 0 }, + secondary: { usedPercent: 97, resetAtMs: 61_000 }, + updatedAt: 1_000, + }); + scheduler.markRateLimited("acc:model", 30_000, 1_000); + const internalSnapshots = ( + scheduler as unknown as { + snapshots: Map; + } + ).snapshots; + expect(internalSnapshots.get("acc:model")?.secondary.usedPercent).toBe(97); + expect(internalSnapshots.get("acc:model")?.secondary.resetAtMs).toBe(61_000); + }); + it("sanitizes non-finite retry-after values", () => { const scheduler = new PreemptiveQuotaScheduler(); scheduler.markRateLimited("acc:model", Number.NaN, 1_000); From ad665968aa1c14554ea7d93070003056af50efa1 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 5 Apr 2026 11:20:40 +0800 Subject: [PATCH 17/18] fix: harden rate-limit and wrapper recovery --- index.ts | 11 +- lib/config.ts | 7 +- lib/request/fetch-helpers.ts | 7 ++ lib/storage/flagged-storage-io.ts | 29 ++++++ lib/unified-settings.ts | 20 ++-- scripts/codex.js | 112 ++++++++++++++++++-- test/codex-bin-wrapper.test.ts | 145 ++++++++++++++++++++++---- test/codex-manager-cli.test.ts | 2 +- test/config-save.test.ts | 36 +++++++ test/fetch-helpers.test.ts | 21 ++++ test/index.test.ts | 153 +++++++++++++++++++++------- test/settings-hub-utils.test.ts | 2 +- test/storage-flagged.test.ts | 38 ++++++- test/storage-recovery-paths.test.ts | 3 +- 14 files changed, 504 insertions(+), 82 deletions(-) diff --git a/index.ts b/index.ts index c3a0be049..da086e20e 100644 --- a/index.ts +++ b/index.ts @@ -1858,16 +1858,18 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { break; } - if (rateLimit) { + if (response.status === 429) { runtimeMetrics.rateLimitedResponses++; + const retryAfterMs = + rateLimit?.retryAfterMs ?? 60_000; const { attempt, delayMs } = getRateLimitBackoff( account.index, quotaKey, - rateLimit.retryAfterMs, + retryAfterMs, ); const cooldownMs = Math.max( delayMs, - rateLimit.retryAfterMs, + retryAfterMs, ); preemptiveQuotaScheduler.markRateLimited( quotaScheduleKey, @@ -1900,7 +1902,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { account, cooldownMs, modelFamily, - parseRateLimitReason(rateLimit.code), + parseRateLimitReason(rateLimit?.code), model, ); accountManager.recordRateLimit( @@ -2183,6 +2185,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { modelFamily, model, ); + accountManager.saveToDiskDebounced(); } else { accountManager.recordFailure( fallbackAccount, diff --git a/lib/config.ts b/lib/config.ts index 4e0a676c2..b82f0731d 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -481,7 +481,7 @@ async function readConfigRecordForSave( return { status: "invalid", errorMessage }; } return { status: "ok", record: parsed }; - } catch (error) { + } catch (error) { const code = (error as NodeJS.ErrnoException | undefined)?.code; if (code === "ENOENT") { return { status: "missing" }; @@ -498,7 +498,10 @@ async function readConfigRecordForSave( error instanceof Error ? error.message : String(error) }`; logConfigWarnOnce(errorMessage); - if (typeof code === "string" && RETRYABLE_CONFIG_READ_CODES.has(code)) { + if (error instanceof SyntaxError) { + return { status: "invalid", errorMessage }; + } + if (typeof code === "string") { return { status: "unreadable", errorMessage }; } return { status: "invalid", errorMessage }; diff --git a/lib/request/fetch-helpers.ts b/lib/request/fetch-helpers.ts index 05808c14a..6db96bd1f 100644 --- a/lib/request/fetch-helpers.ts +++ b/lib/request/fetch-helpers.ts @@ -1240,6 +1240,13 @@ function parseRetryAfterMs( if (normalized !== null) { return normalized; } + const parsedDate = Date.parse(retryAfterHeader); + if (Number.isFinite(parsedDate)) { + const normalizedDate = normalizeRetryAfterMs(parsedDate - Date.now()); + if (normalizedDate !== null) { + return normalizedDate; + } + } } const resetAfterSecondsHeaders = [ diff --git a/lib/storage/flagged-storage-io.ts b/lib/storage/flagged-storage-io.ts index 4c4837935..45b0b5abe 100644 --- a/lib/storage/flagged-storage-io.ts +++ b/lib/storage/flagged-storage-io.ts @@ -4,6 +4,25 @@ import type { FlaggedAccountStorageV1 } from "../storage.js"; const RETRYABLE_UNLINK_CODES = new Set(["EBUSY", "EAGAIN", "EPERM"]); +function isValidFlaggedStorageCandidate( + data: unknown, + storage: FlaggedAccountStorageV1, +): boolean { + if ( + !data || + typeof data !== "object" || + !Object.hasOwn(data, "version") || + !Object.hasOwn(data, "accounts") + ) { + return false; + } + const candidate = data as { version?: unknown; accounts?: unknown }; + if (candidate.version !== 1 || !Array.isArray(candidate.accounts)) { + return false; + } + return candidate.accounts.length === 0 || storage.accounts.length > 0; +} + /** * Return the ordered backup paths consulted for flagged-account recovery. */ @@ -55,6 +74,13 @@ export async function loadFlaggedAccountsState(params: { const backupContent = await fs.readFile(backupPath, "utf-8"); const backupData = JSON.parse(backupContent) as unknown; const recovered = params.normalizeFlaggedStorage(backupData); + if (!isValidFlaggedStorageCandidate(backupData, recovered)) { + params.logError("Skipping invalid flagged account backup payload", { + from: backupPath, + to: params.path, + }); + continue; + } if (existsSync(params.resetMarkerPath)) { return empty; } @@ -79,6 +105,9 @@ export async function loadFlaggedAccountsState(params: { const content = await fs.readFile(params.path, "utf-8"); const data = JSON.parse(content) as unknown; const loaded = params.normalizeFlaggedStorage(data); + if (!isValidFlaggedStorageCandidate(data, loaded)) { + throw new Error("Invalid flagged account storage payload"); + } if (existsSync(params.resetMarkerPath)) { return empty; } diff --git a/lib/unified-settings.ts b/lib/unified-settings.ts index 3444c01ee..c8c703ef7 100644 --- a/lib/unified-settings.ts +++ b/lib/unified-settings.ts @@ -68,10 +68,14 @@ function parseSettingsRecord(content: string): JsonRecord { * cannot be parsed into the expected object shape. */ function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null { - if (!existsSync(filePath)) { - return null; + try { + return parseSettingsRecord(readFileSync(filePath, "utf8")); + } catch (error) { + if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { + return null; + } + throw error; } - return parseSettingsRecord(readFileSync(filePath, "utf8")); } /** @@ -80,10 +84,14 @@ function readSettingsRecordSyncFromPath(filePath: string): JsonRecord | null { async function readSettingsRecordAsyncFromPath( filePath: string, ): Promise { - if (!existsSync(filePath)) { - return null; + try { + return parseSettingsRecord(await fs.readFile(filePath, "utf8")); + } catch (error) { + if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { + return null; + } + throw error; } - return parseSettingsRecord(await fs.readFile(filePath, "utf8")); } /** diff --git a/scripts/codex.js b/scripts/codex.js index 63eac3b20..7c515b289 100755 --- a/scripts/codex.js +++ b/scripts/codex.js @@ -7,6 +7,7 @@ import { existsSync, mkdirSync, mkdtempSync, + renameSync, readFileSync, rmSync, statSync, @@ -215,6 +216,47 @@ const REASONING_FALLBACKS = { }; const KNOWN_REASONING_EFFORTS = new Set(Object.keys(REASONING_FALLBACKS)); +const REQUESTED_MODEL_ALIASES = new Map(); + +function addRequestedModelAlias(alias, normalizedModel) { + REQUESTED_MODEL_ALIASES.set(alias, normalizedModel); +} + +function addRequestedModelReasoningAliases(alias, normalizedModel) { + addRequestedModelAlias(alias, normalizedModel); + for (const effort of KNOWN_REASONING_EFFORTS) { + addRequestedModelAlias(`${alias}-${effort}`, normalizedModel); + } +} + +function seedRequestedModelAliases() { + addRequestedModelReasoningAliases("gpt-5.4", "gpt-5.4"); + addRequestedModelReasoningAliases("gpt-5.4-pro", "gpt-5.4-pro"); + addRequestedModelReasoningAliases("gpt-5.2-pro", "gpt-5.2-pro"); + addRequestedModelReasoningAliases("gpt-5-pro", "gpt-5-pro"); + addRequestedModelReasoningAliases("gpt-5.2", "gpt-5.2"); + addRequestedModelReasoningAliases("gpt-5.1", "gpt-5.1"); + addRequestedModelReasoningAliases("gpt-5", "gpt-5"); + addRequestedModelReasoningAliases("gpt-5-mini", "gpt-5-mini"); + addRequestedModelReasoningAliases("gpt-5-nano", "gpt-5-nano"); + addRequestedModelReasoningAliases("gpt-5.1-chat-latest", "gpt-5.1"); + addRequestedModelReasoningAliases("gpt-5-chat-latest", "gpt-5"); + addRequestedModelReasoningAliases("gpt-5.4-mini", "gpt-5-mini"); + addRequestedModelReasoningAliases("gpt-5.4-nano", "gpt-5-nano"); + addRequestedModelReasoningAliases("gpt-5-codex", "gpt-5-codex"); + addRequestedModelReasoningAliases("gpt-5.3-codex-spark", "gpt-5-codex"); + addRequestedModelReasoningAliases("gpt-5.3-codex", "gpt-5-codex"); + addRequestedModelReasoningAliases("gpt-5.2-codex", "gpt-5-codex"); + addRequestedModelReasoningAliases("gpt-5.1-codex", "gpt-5-codex"); + addRequestedModelAlias("gpt_5_codex", "gpt-5-codex"); + addRequestedModelReasoningAliases("codex-max", "gpt-5.1-codex-max"); + addRequestedModelReasoningAliases("gpt-5.1-codex-max", "gpt-5.1-codex-max"); + addRequestedModelAlias("codex-mini-latest", "gpt-5.1-codex-mini"); + addRequestedModelReasoningAliases("gpt-5-codex-mini", "gpt-5.1-codex-mini"); + addRequestedModelReasoningAliases("gpt-5.1-codex-mini", "gpt-5.1-codex-mini"); +} + +seedRequestedModelAliases(); function stripProviderPrefix(model) { return typeof model === "string" && model.includes("/") @@ -226,6 +268,10 @@ function normalizeRequestedModel(model) { const stripped = stripProviderPrefix(model ?? ""); const normalized = stripped.trim().toLowerCase(); if (normalized.length === 0) return ""; + const exactMatch = REQUESTED_MODEL_ALIASES.get(normalized); + if (exactMatch) { + return exactMatch; + } if ( normalized.includes("gpt-5.1-codex-max") || @@ -442,6 +488,47 @@ function ensureTrailingNewline(value) { return value.endsWith("\n") ? value : `${value}\n`; } +function captureShadowHomeState(filePath) { + try { + if (!existsSync(filePath)) { + return { exists: false, content: null }; + } + return { + exists: true, + content: readFileSync(filePath, "utf8"), + }; + } catch { + return { exists: true, content: null, unreadable: true }; + } +} + +function shadowHomeStateMatches(left, right) { + return ( + left.exists === right.exists && + left.content === right.content && + Boolean(left.unreadable) === Boolean(right.unreadable) + ); +} + +function syncShadowHomeStateFile(sourcePath, destinationPath) { + const tempPath = join( + dirname(destinationPath), + `.${basename(destinationPath)}.codex-multi-auth-sync-${process.pid}.tmp`, + ); + try { + mkdirSync(dirname(destinationPath), { recursive: true }); + copyFileSync(sourcePath, tempPath); + renameSync(tempPath, destinationPath); + } catch (error) { + try { + rmSync(tempPath, { force: true }); + } catch { + // Best-effort cleanup only. + } + throw error; + } +} + function rewriteConfigTomlReasoningEffort(rawConfig, requestedModel) { const lineEnding = rawConfig.includes("\r\n") ? "\r\n" : "\n"; let changed = false; @@ -503,6 +590,12 @@ function createCompatibilityCodexHome( if (!existsSync(configPath)) { return { args: processedArgs, env: baseEnv, cleanup: undefined }; } + const originalShadowHomeState = new Map( + SHADOW_HOME_STATE_FILES.map((name) => [ + name, + captureShadowHomeState(join(originalCodexHome, name)), + ]), + ); const rawConfig = readFileSync(configPath, "utf8"); const compatConfig = rewriteConfigTomlReasoningEffort( @@ -531,20 +624,23 @@ function createCompatibilityCodexHome( const syncShadowHomeStateBack = () => { for (const name of SHADOW_HOME_STATE_FILES) { const shadowPath = join(shadowCodexHome, name); - if (!existsSync(shadowPath)) { + const shadowState = captureShadowHomeState(shadowPath); + if (!shadowState.exists || shadowState.unreadable) { continue; } try { - const shadowStats = statSync(shadowPath); const originalPath = join(originalCodexHome, name); - if (existsSync(originalPath)) { - const originalStats = statSync(originalPath); - if (originalStats.isFile() && originalStats.mtimeMs > shadowStats.mtimeMs) { - continue; - } + const originalSnapshot = + originalShadowHomeState.get(name) ?? { exists: false, content: null }; + const currentOriginalState = captureShadowHomeState(originalPath); + if (!shadowHomeStateMatches(currentOriginalState, originalSnapshot)) { + continue; + } + if (shadowHomeStateMatches(shadowState, originalSnapshot)) { + continue; } - copyFileSync(shadowPath, originalPath); + syncShadowHomeStateFile(shadowPath, originalPath); tightenShadowHomePermissions(originalPath); } catch { // Best-effort only; runtime auth refreshes should not fail cleanup. diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index 56685e6b9..0568a0927 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -120,12 +120,46 @@ function createSpawnSyncSuccess(stdout: string): SpawnSyncReturns { }; } +const WRAPPER_ENV_ALLOWLIST = [ + "APPDATA", + "CI", + "COLORTERM", + "COMSPEC", + "ComSpec", + "HOME", + "HOMEDRIVE", + "HOMEPATH", + "LANG", + "LOCALAPPDATA", + "NODE_OPTIONS", + "OS", + "PATH", + "Path", + "PATHEXT", + "PROCESSOR_ARCHITECTURE", + "PROGRAMDATA", + "ProgramData", + "SYSTEMROOT", + "SystemRoot", + "TEMP", + "TERM", + "TERM_PROGRAM", + "TMP", + "TMPDIR", + "USERPROFILE", + "WINDIR", +] as const; + function buildWrapperEnv(extraEnv: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - const env: NodeJS.ProcessEnv = { - ...process.env, - CODEX_MULTI_AUTH_FORCE_FILE_AUTH_STORE: "1", - ...extraEnv, - }; + const env: NodeJS.ProcessEnv = {}; + for (const key of WRAPPER_ENV_ALLOWLIST) { + const value = process.env[key]; + if (value !== undefined) { + env[key] = value; + } + } + env.CODEX_MULTI_AUTH_FORCE_FILE_AUTH_STORE = "1"; + Object.assign(env, extraEnv); for (const [key, value] of Object.entries(env)) { if (value === undefined) { delete env[key]; @@ -405,9 +439,9 @@ describe("codex bin wrapper", () => { ); expect(result.status).toBe(1); - expect( - readdirSync(controlledTmp).filter((entry) => - entry.startsWith("codex-multi-auth-home-"), + expect( + readdirSync(controlledTmp).filter((entry) => + entry.startsWith("codex-multi-auth-home-"), ), ).toEqual([]); }); @@ -449,12 +483,56 @@ describe("codex bin wrapper", () => { expect(result.status).toBe(0); expect(readFileSync(join(originalHome, "auth.json"), "utf8").trim()).toBe('{"token":"shadow"}'); expect(readFileSync(join(originalHome, "accounts.json"), "utf8").trim()).toBe('{"accounts":["shadow"]}'); + expect(readFileSync(join(originalHome, ".codex-global-state.json"), "utf8").trim()).toBe('{"last":"shadow"}'); + expect( + readdirSync(controlledTmp).filter((entry) => + entry.startsWith("codex-multi-auth-home-"), + ), + ).toEqual([]); + }); + + it("does not clobber original auth state that changed while the compatibility shadow was active", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ + "#!/usr/bin/env node", + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'const home = process.env.CODEX_HOME ?? "";', + 'const originalHome = process.env.CODEX_MULTI_AUTH_TEST_EXTERNAL_HOME ?? "";', + 'fs.writeFileSync(path.join(home, "auth.json"), \'{"token":"shadow"}\\n\', "utf8");', + 'fs.writeFileSync(path.join(home, "accounts.json"), \'{"accounts":["shadow"]}\\n\', "utf8");', + 'fs.writeFileSync(path.join(home, ".codex-global-state.json"), \'{"last":"shadow"}\\n\', "utf8");', + 'if (originalHome) {', + ' fs.writeFileSync(path.join(originalHome, "auth.json"), \'{"token":"external"}\\n\', "utf8");', + '}', + "process.exit(0);", + ]); + const originalHome = join(fixtureRoot, "codex-home"); + const controlledTmp = join(fixtureRoot, "tmp"); + mkdirSync(originalHome, { recursive: true }); + mkdirSync(controlledTmp, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), '{"token":"original"}\n', "utf8"); + writeFileSync(join(originalHome, "accounts.json"), '{"accounts":["original"]}\n', "utf8"); + writeFileSync(join(originalHome, ".codex-global-state.json"), '{"last":"original"}\n', "utf8"); + writeFileSync(join(originalHome, "config.toml"), 'model_reasoning_effort = "xhigh"\n', "utf8"); + + const result = runWrapper( + fixtureRoot, + ["exec", "status", "--model", "gpt-5.1"], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + CODEX_MULTI_AUTH_TEST_EXTERNAL_HOME: originalHome, + TMP: controlledTmp, + TEMP: controlledTmp, + TMPDIR: controlledTmp, + }, + ); + + expect(result.status).toBe(0); + expect(readFileSync(join(originalHome, "auth.json"), "utf8").trim()).toBe('{"token":"external"}'); + expect(readFileSync(join(originalHome, "accounts.json"), "utf8").trim()).toBe('{"accounts":["shadow"]}'); expect(readFileSync(join(originalHome, ".codex-global-state.json"), "utf8").trim()).toBe('{"last":"shadow"}'); - expect( - readdirSync(controlledTmp).filter((entry) => - entry.startsWith("codex-multi-auth-home-"), - ), - ).toEqual([]); }); it("rewrites unquoted config reasoning effort values for mini compatibility models", () => { @@ -561,10 +639,43 @@ describe("codex bin wrapper", () => { ); expect(result.status).toBe(0); - expect(result.stdout).toContain( - 'FORWARDED:exec status --model gpt-5.1-codex-mini -c model_reasoning_effort="high" -c cli_auth_credentials_store="file"', - ); - expect(result.stdout).not.toContain('model_reasoning_effort="xhigh"'); + expect(result.stdout).toContain( + 'FORWARDED:exec status --model gpt-5.1-codex-mini -c model_reasoning_effort="high" -c cli_auth_credentials_store="file"', + ); + expect(result.stdout).not.toContain('model_reasoning_effort="xhigh"'); + }); + + it("coerces reasoning overrides for reasoning-suffixed general-model aliases", () => { + const fixtureRoot = createWrapperFixture(); + const fakeBin = createFakeCodexBin(fixtureRoot); + const originalHome = join(fixtureRoot, "codex-home"); + mkdirSync(originalHome, { recursive: true }); + writeFileSync(join(originalHome, "auth.json"), "{}\n", "utf8"); + writeFileSync(join(originalHome, "config.toml"), "", "utf8"); + + for (const model of ["gpt-5-low", "gpt-5-chat-latest-low"]) { + const result = runWrapper( + fixtureRoot, + [ + "exec", + "status", + "--model", + model, + "-c", + 'model_reasoning_effort="xhigh"', + ], + { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + `FORWARDED:exec status --model ${model} -c model_reasoning_effort="high" -c cli_auth_credentials_store="file"`, + ); + expect(result.stdout).not.toContain('model_reasoning_effort="xhigh"'); + } }); it("preserves explicit xhigh overrides for models that support them", () => { diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index bf0dfe252..ca9c44c32 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -7274,7 +7274,7 @@ describe("codex manager cli commands", () => { loadDashboardDisplaySettingsMock.mockResolvedValue( createReadyFirstMenuSettings({ menuAutoFetchLimits: true, - menuQuotaTtlMs: 1, + menuQuotaTtlMs: 0, menuShowFetchStatus: true, }), ); diff --git a/test/config-save.test.ts b/test/config-save.test.ts index 3b4222c98..5c33bbc05 100644 --- a/test/config-save.test.ts +++ b/test/config-save.test.ts @@ -219,6 +219,42 @@ describe("plugin config save paths", () => { expect(parsed).toEqual({ codexMode: true, preserved: 1 }); }); + it("treats non-retryable env-path read failures as unreadable", async () => { + const configPath = join(tempDir, "plugin-config.json"); + process.env.CODEX_MULTI_AUTH_CONFIG_PATH = configPath; + await fs.writeFile( + configPath, + JSON.stringify({ codexMode: true, preserved: 1 }), + "utf8", + ); + + const originalReadFile = fs.readFile.bind(fs); + const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { + const [targetPath] = args; + if (targetPath === configPath) { + const error = new Error("permission denied") as NodeJS.ErrnoException; + error.code = "EACCES"; + throw error; + } + return originalReadFile(...args); + }); + + try { + const { savePluginConfig } = await import("../lib/config.js"); + await expect(savePluginConfig({ codexMode: false })).rejects.toThrow( + "unreadable", + ); + } finally { + readSpy.mockRestore(); + } + + const parsed = JSON.parse(await fs.readFile(configPath, "utf8")) as Record< + string, + unknown + >; + expect(parsed).toEqual({ codexMode: true, preserved: 1 }); + }); + it("does not overwrite an unreadable unified settings file", async () => { delete process.env.CODEX_MULTI_AUTH_CONFIG_PATH; const unifiedPath = join(tempDir, "settings.json"); diff --git a/test/fetch-helpers.test.ts b/test/fetch-helpers.test.ts index 3cfb1581e..3b5f9a28a 100644 --- a/test/fetch-helpers.test.ts +++ b/test/fetch-helpers.test.ts @@ -1072,6 +1072,27 @@ describe('createEntitlementErrorResponse', () => { expect(rateLimit?.retryAfterMs).toBe(10000); }); + it('parses retry-after header (http date)', async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date('2026-04-05T00:00:00.000Z')); + const headers = new Headers({ + 'retry-after': new Date('2026-04-05T00:01:30.000Z').toUTCString(), + }); + const response = new Response( + JSON.stringify({ error: { message: 'rate limited' } }), + { status: 429, headers }, + ); + + const { rateLimit } = await handleErrorResponse(response); + + expect(rateLimit).toBeDefined(); + expect(rateLimit?.retryAfterMs).toBe(90_000); + } finally { + vi.useRealTimers(); + } + }); + it('parses x-ratelimit-reset header (unix timestamp)', async () => { const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const headers = new Headers({ 'x-ratelimit-reset': String(futureTimestamp) }); diff --git a/test/index.test.ts b/test/index.test.ts index 8678d8996..62b7fca83 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -4260,8 +4260,98 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { expect(showRuntimeToastMock).not.toHaveBeenCalled(); }); + it("applies the default cooldown when a 429 has no parsed retry metadata", async () => { + const { AccountManager } = await import("../lib/accounts.js"); + const fetchHelpersModule = await import("../lib/request/fetch-helpers.js"); + const rateLimitBackoffModule = await import("../lib/request/rate-limit-backoff.js"); + + const markRateLimitedWithReason = vi.fn(); + const manager = { + getAccountCount: () => 1, + getCurrentOrNextForFamilyHybrid: () => ({ + index: 0, + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + }), + getCurrentOrNextForFamily: () => ({ + index: 0, + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + }), + getCurrentWorkspace: () => null, + getAccountByIndex: () => null, + getAccountsSnapshot: () => [], + isAccountAvailableForFamily: () => true, + toAuthDetails: () => ({ + type: "oauth" as const, + access: "access-token", + refresh: "refresh-1", + expires: Date.now() + 60_000, + }), + hasRefreshToken: () => true, + saveToDiskDebounced: () => {}, + updateFromAuth: () => {}, + clearAuthFailures: () => {}, + incrementAuthFailures: () => 1, + saveToDisk: async () => {}, + markAccountCoolingDown: () => {}, + markRateLimited: () => {}, + markRateLimitedWithReason, + consumeToken: () => true, + refundToken: () => {}, + syncCodexCliActiveSelectionForIndex: async () => {}, + markSwitched: () => {}, + removeAccount: () => {}, + recordFailure: () => {}, + recordSuccess: () => {}, + recordRateLimit: () => {}, + getMinWaitTimeForFamily: () => 0, + shouldShowAccountToast: () => false, + markToastShown: () => {}, + setActiveIndex: () => null, + }; + vi.spyOn(AccountManager, "loadFromDisk").mockResolvedValue(manager as never); + vi.mocked(fetchHelpersModule.handleErrorResponse).mockResolvedValueOnce({ + response: new Response("rate limited", { status: 429 }), + rateLimit: undefined, + errorBody: "rate limited", + } as never); + vi.mocked(rateLimitBackoffModule.getRateLimitBackoff).mockReturnValueOnce({ + attempt: 1, + delayMs: 5_000, + }); + globalThis.fetch = vi + .fn() + .mockResolvedValueOnce(new Response("rate limited", { status: 429 })); + + const mockClient = createMockClient(); + const { OpenAIOAuthPlugin } = await import("../index.js"); + const plugin = await OpenAIOAuthPlugin({ client: mockClient } as never) as unknown as PluginType; + const sdk = await plugin.auth.loader(getOAuthAuth, { options: {}, models: {} }); + const response = await sdk.fetch!("https://api.openai.com/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ model: "gpt-5.1" }), + }); + + expect(response.status).toBe(503); + expect(markRateLimitedWithReason).toHaveBeenCalledWith( + expect.objectContaining({ index: 0 }), + 60_000, + "gpt-5.1", + "unknown", + "gpt-5.1", + ); + }); + it("persists the longer parsed rate-limit cooldown across overlapping requests", async () => { const { AccountManager } = await import("../lib/accounts.js"); + const { AccountManager: ActualAccountManager } = + await vi.importActual( + "../lib/accounts.js", + ); + const { MODEL_FAMILIES } = await import("../lib/prompts/codex.js"); const fetchHelpersModule = await import("../lib/request/fetch-helpers.js"); const rateLimitBackoffModule = await import("../lib/request/rate-limit-backoff.js"); const longCooldownMs = 90 * 60 * 1000; @@ -4269,43 +4359,27 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { vi.useFakeTimers(); try { vi.setSystemTime(new Date("2026-04-05T00:00:00.000Z")); - const requestAccount = { - index: 0, - accountId: "acc-1", - email: "alpha@example.com", - refreshToken: "refresh-1", - rateLimitResetTimes: {} as Record, - lastSwitchReason: undefined as string | undefined, - }; - let observedBaseResetAt: number | undefined; + const actualManager = new ActualAccountManager(undefined, { + version: 3, + accounts: [ + { + accountId: "acc-1", + email: "alpha@example.com", + refreshToken: "refresh-1", + accessToken: "access-token", + expiresAt: Date.now() + 60_000, + addedAt: 1, + lastUsed: 1, + }, + ], + activeIndex: 0, + activeIndexByFamily: Object.fromEntries( + MODEL_FAMILIES.map((family) => [family, 0]), + ) as Record, + } as never); + const requestAccount = actualManager.getCurrentAccount()!; const markRateLimitedWithReason = vi.fn( - ( - account: { rateLimitResetTimes: Record }, - retryAfterMs: number, - family: string, - reason: string, - model?: string | null, - ) => { - const resetAt = Date.now() + retryAfterMs; - const baseKey = family; - if (!model || reason === "quota" || reason === "unknown") { - account.rateLimitResetTimes[baseKey] = Math.max( - account.rateLimitResetTimes[baseKey] ?? 0, - resetAt, - ); - observedBaseResetAt = account.rateLimitResetTimes[baseKey]; - } - if ( - model && - (reason === "tokens" || reason === "concurrent" || reason === "unknown") - ) { - const modelKey = `${family}:${model}`; - account.rateLimitResetTimes[modelKey] = Math.max( - account.rateLimitResetTimes[modelKey] ?? 0, - resetAt, - ); - } - }, + actualManager.markRateLimitedWithReason.bind(actualManager), ); const manager = { getAccountCount: () => 1, @@ -4415,7 +4489,8 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { ]); expect(markRateLimitedWithReason).toHaveBeenCalledTimes(2); - expect(observedBaseResetAt).toBe( + const familyKey = markRateLimitedWithReason.mock.calls[0]?.[2] as string; + expect(requestAccount.rateLimitResetTimes[familyKey]).toBe( Date.parse("2026-04-05T01:30:00.000Z"), ); } finally { @@ -4444,6 +4519,7 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { }; const markRateLimitedWithReason = vi.fn(); const recordRateLimit = vi.fn(); + const saveToDiskDebounced = vi.fn(); const pendingFailovers: Array> = []; const fallback429Response = new Response( new ReadableStream({ @@ -4471,7 +4547,7 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { expires: Date.now() + 60_000, }), hasRefreshToken: () => true, - saveToDiskDebounced: () => {}, + saveToDiskDebounced, updateFromAuth: () => {}, clearAuthFailures: () => {}, incrementAuthFailures: () => 1, @@ -4555,6 +4631,7 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => { "gpt-5.1", "gpt-5.1", ); + expect(saveToDiskDebounced).toHaveBeenCalledTimes(1); expect(fallbackCancelSpy).toHaveBeenCalledTimes(1); }); diff --git a/test/settings-hub-utils.test.ts b/test/settings-hub-utils.test.ts index af3bc055a..36ca1e3bf 100644 --- a/test/settings-hub-utils.test.ts +++ b/test/settings-hub-utils.test.ts @@ -232,7 +232,7 @@ describe("settings-hub utility coverage", () => { expect(() => api.clampBackendNumber("unknown-setting", 5)).toThrow( "Unknown backend numeric setting key", ); - }, 15_000); + }); it("formats layout mode labels", async () => { const api = await loadSettingsHubTestApi(); diff --git a/test/storage-flagged.test.ts b/test/storage-flagged.test.ts index 2e624e222..9f92b21fe 100644 --- a/test/storage-flagged.test.ts +++ b/test/storage-flagged.test.ts @@ -333,12 +333,42 @@ describe("flagged account storage", () => { return originalReadFile(...args); }); + const flagged = await loadFlaggedAccounts(); + expect(flagged.accounts).toHaveLength(1); + expect(flagged.accounts[0]?.refreshToken).toBe("primary-flagged"); + expect(existsSync(flaggedPath)).toBe(true); + + readSpy.mockRestore(); + }); + + it("skips invalid latest flagged backups and falls back to older valid snapshots", async () => { + const flaggedPath = getFlaggedAccountsPath(); + await fs.mkdir(dirname(flaggedPath), { recursive: true }); + await fs.writeFile( + `${flaggedPath}.bak`, + JSON.stringify({ version: 99, accounts: "broken" }), + "utf8", + ); + await fs.writeFile( + `${flaggedPath}.bak.1`, + JSON.stringify({ + version: 1, + accounts: [ + { + refreshToken: "valid-older-backup", + flaggedAt: 3, + addedAt: 3, + lastUsed: 3, + }, + ], + }), + "utf8", + ); + const flagged = await loadFlaggedAccounts(); - expect(flagged.accounts).toHaveLength(1); - expect(flagged.accounts[0]?.refreshToken).toBe("primary-flagged"); - expect(existsSync(flaggedPath)).toBe(true); - readSpy.mockRestore(); + expect(flagged.accounts).toHaveLength(1); + expect(flagged.accounts[0]?.refreshToken).toBe("valid-older-backup"); }); it("honors the reset marker even when it appears during backup recovery", async () => { diff --git a/test/storage-recovery-paths.test.ts b/test/storage-recovery-paths.test.ts index f93d9e29d..123a7efde 100644 --- a/test/storage-recovery-paths.test.ts +++ b/test/storage-recovery-paths.test.ts @@ -181,11 +181,12 @@ describe("storage recovery paths", () => { ); const originalReadFile = fs.readFile.bind(fs); + const originalWriteFile = fs.writeFile.bind(fs); const readSpy = vi.spyOn(fs, "readFile").mockImplementation(async (...args) => { const [targetPath] = args; const result = await originalReadFile(...args); if (targetPath === backupPath) { - await fs.writeFile(resetMarkerPath, "reset", "utf-8"); + await originalWriteFile(resetMarkerPath, "reset", "utf-8"); } return result; }); From 9e571995e2e5d40deff9a34dee7f9dc333fc844e Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 5 Apr 2026 11:28:47 +0800 Subject: [PATCH 18/18] fix: persist recovered flagged backups --- lib/storage.ts | 8 ++++ lib/storage/flagged-load-entry.ts | 9 ++++ lib/storage/flagged-storage-io.ts | 21 +++++++++ test/rotation-integration.test.ts | 24 ++++++++++- test/storage-recovery-paths.test.ts | 10 +++++ test/unified-settings.test.ts | 66 +++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 1 deletion(-) diff --git a/lib/storage.ts b/lib/storage.ts index 931545132..e744bfbde 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -1806,6 +1806,14 @@ export async function loadFlaggedAccounts(): Promise { isRecord, now: () => Date.now(), }), + persistRecoveredBackup: async (storage, resetMarkerPath) => + withStorageLock(async () => { + if (existsSync(resetMarkerPath)) { + return false; + } + await saveFlaggedAccountsUnlocked(storage); + return true; + }), saveFlaggedAccounts, loadFlaggedAccountsState, logError: (message, details) => { diff --git a/lib/storage/flagged-load-entry.ts b/lib/storage/flagged-load-entry.ts index 8da8eabc1..aa2b0a237 100644 --- a/lib/storage/flagged-load-entry.ts +++ b/lib/storage/flagged-load-entry.ts @@ -5,12 +5,20 @@ export async function loadFlaggedAccountsEntry(params: { getLegacyFlaggedAccountsPath: () => string; getIntentionalResetMarkerPath: (path: string) => string; normalizeFlaggedStorage: (data: unknown) => FlaggedAccountStorageV1; + persistRecoveredBackup: ( + storage: FlaggedAccountStorageV1, + resetMarkerPath: string, + ) => Promise; saveFlaggedAccounts: (storage: FlaggedAccountStorageV1) => Promise; loadFlaggedAccountsState: (args: { path: string; legacyPath: string; resetMarkerPath: string; normalizeFlaggedStorage: (data: unknown) => FlaggedAccountStorageV1; + persistRecoveredBackup: ( + storage: FlaggedAccountStorageV1, + resetMarkerPath: string, + ) => Promise; saveFlaggedAccounts: (storage: FlaggedAccountStorageV1) => Promise; logError: (message: string, details: Record) => void; logInfo: (message: string, details: Record) => void; @@ -24,6 +32,7 @@ export async function loadFlaggedAccountsEntry(params: { legacyPath: params.getLegacyFlaggedAccountsPath(), resetMarkerPath: params.getIntentionalResetMarkerPath(path), normalizeFlaggedStorage: params.normalizeFlaggedStorage, + persistRecoveredBackup: params.persistRecoveredBackup, saveFlaggedAccounts: params.saveFlaggedAccounts, logError: params.logError, logInfo: params.logInfo, diff --git a/lib/storage/flagged-storage-io.ts b/lib/storage/flagged-storage-io.ts index 45b0b5abe..a866d86bd 100644 --- a/lib/storage/flagged-storage-io.ts +++ b/lib/storage/flagged-storage-io.ts @@ -57,6 +57,10 @@ export async function loadFlaggedAccountsState(params: { legacyPath: string; resetMarkerPath: string; normalizeFlaggedStorage: (data: unknown) => FlaggedAccountStorageV1; + persistRecoveredBackup: ( + storage: FlaggedAccountStorageV1, + resetMarkerPath: string, + ) => Promise; saveFlaggedAccounts: (storage: FlaggedAccountStorageV1) => Promise; logError: (message: string, details: Record) => void; logInfo: (message: string, details: Record) => void; @@ -84,6 +88,23 @@ export async function loadFlaggedAccountsState(params: { if (existsSync(params.resetMarkerPath)) { return empty; } + if (recovered.accounts.length > 0) { + try { + const persisted = await params.persistRecoveredBackup( + recovered, + params.resetMarkerPath, + ); + if (!persisted) { + return empty; + } + } catch (persistError) { + params.logError("Failed to persist recovered flagged account storage", { + from: backupPath, + to: params.path, + error: String(persistError), + }); + } + } params.logInfo("Recovered flagged account storage from backup", { from: backupPath, to: params.path, diff --git a/test/rotation-integration.test.ts b/test/rotation-integration.test.ts index 4f6716ac3..f0593c6b4 100644 --- a/test/rotation-integration.test.ts +++ b/test/rotation-integration.test.ts @@ -1,7 +1,7 @@ import { promises as fs } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { describe, it, expect, beforeAll, beforeEach, afterAll } from "vitest"; +import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from "vitest"; import { AccountManager } from "../lib/accounts.js"; import { deduplicateAccounts, @@ -303,6 +303,28 @@ describe("Multi-Account Rotation Integration", () => { TEST_ACCOUNTS.slice(0, 3).map((account) => account.email), ); }, 15_000); + + it("removeWithRetry retries transient windows fs.rm errors", async () => { + const rmSpy = vi.spyOn(fs, "rm"); + const retryCodes = ["EBUSY", "EPERM", "ENOTEMPTY"]; + for (const code of retryCodes) { + rmSpy.mockImplementationOnce(async () => { + const error = new Error(code) as NodeJS.ErrnoException; + error.code = code; + throw error; + }); + } + + let callCount = 0; + try { + await removeWithRetry(testStoragePath, { force: true }); + callCount = rmSpy.mock.calls.length; + } finally { + rmSpy.mockRestore(); + } + + expect(callCount).toBe(retryCodes.length + 1); + }); }); describe("Debounced save", () => { diff --git a/test/storage-recovery-paths.test.ts b/test/storage-recovery-paths.test.ts index 123a7efde..5d0b9babc 100644 --- a/test/storage-recovery-paths.test.ts +++ b/test/storage-recovery-paths.test.ts @@ -132,6 +132,11 @@ describe("storage recovery paths", () => { const recovered = await loadFlaggedAccounts(); expect(recovered.accounts).toHaveLength(1); expect(recovered.accounts[0]?.email).toBe("flagged@example.com"); + + const persisted = JSON.parse(await fs.readFile(flaggedPath, "utf-8")) as { + accounts?: Array<{ email?: string }>; + }; + expect(persisted.accounts?.[0]?.email).toBe("flagged@example.com"); }); it("recovers flagged accounts from backup file when primary storage is unreadable", async () => { @@ -157,6 +162,11 @@ describe("storage recovery paths", () => { const recovered = await loadFlaggedAccounts(); expect(recovered.accounts).toHaveLength(1); expect(recovered.accounts[0]?.email).toBe("flagged2@example.com"); + + const persisted = JSON.parse(await fs.readFile(flaggedPath, "utf-8")) as { + accounts?: Array<{ email?: string }>; + }; + expect(persisted.accounts?.[0]?.email).toBe("flagged2@example.com"); }); it("suppresses flagged backup recovery when the reset marker appears mid-read", async () => { diff --git a/test/unified-settings.test.ts b/test/unified-settings.test.ts index 852863e1c..2681b5ecd 100644 --- a/test/unified-settings.test.ts +++ b/test/unified-settings.test.ts @@ -193,6 +193,72 @@ describe("unified settings", () => { }); }); + it("keeps the last good backup when a concurrent writer updates the primary after a backup-derived read", async () => { + const { + getUnifiedSettingsPath, + loadUnifiedPluginConfigSync, + saveUnifiedPluginConfig, + } = await import("../lib/unified-settings.js"); + + await saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 45_000 }); + await saveUnifiedPluginConfig({ codexMode: false, fetchTimeoutMs: 90_000 }); + await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8"); + + expect(loadUnifiedPluginConfigSync()).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + + const concurrentPrimary = { + version: 1, + pluginConfig: { + codexMode: false, + fetchTimeoutMs: 150_000, + retries: 4, + }, + }; + const copySpy = vi.spyOn(fs, "copyFile"); + const renameSpy = vi.spyOn(fs, "rename"); + let injectedConcurrentWrite = false; + renameSpy.mockImplementationOnce(async () => { + injectedConcurrentWrite = true; + await fs.writeFile( + getUnifiedSettingsPath(), + `${JSON.stringify(concurrentPrimary, null, 2)}\n`, + "utf8", + ); + const error = new Error("denied") as NodeJS.ErrnoException; + error.code = "EACCES"; + throw error; + }); + + try { + await expect( + saveUnifiedPluginConfig({ codexMode: true, fetchTimeoutMs: 120_000 }), + ).rejects.toThrow(); + } finally { + copySpy.mockRestore(); + renameSpy.mockRestore(); + } + + expect(injectedConcurrentWrite).toBe(true); + expect(copySpy).not.toHaveBeenCalledWith( + getUnifiedSettingsPath(), + `${getUnifiedSettingsPath()}.bak`, + ); + const backupRecord = JSON.parse( + await fs.readFile(`${getUnifiedSettingsPath()}.bak`, "utf8"), + ) as { pluginConfig?: Record }; + expect(backupRecord.pluginConfig).toEqual({ + codexMode: true, + fetchTimeoutMs: 45_000, + }); + const primaryRecord = JSON.parse( + await fs.readFile(getUnifiedSettingsPath(), "utf8"), + ) as { pluginConfig?: Record }; + expect(primaryRecord.pluginConfig).toEqual(concurrentPrimary.pluginConfig); + }); + it("resumes snapshotting after a backup-derived write succeeds", async () => { const { getUnifiedSettingsPath,