From ddd993378d2f1065a081b2c8b8acf45eca2040e9 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 22 Sep 2026 22:53:26 +0200 Subject: [PATCH] [Node] migrate @hey-api/openapi-ts" to "0.99.0" --- .../node_web_interface/openapi-ts.config.ts | 25 +- .../node_web_interface/package-lock.json | 488 +++++++++--------- .../node_web_interface/package.json | 2 +- .../src/__tests__/utils.test.ts | 22 +- .../node_web_interface/src/bootstrap-app.tsx | 40 +- .../src/client/client/index.ts | 25 + .../src/client/core/ApiError.ts | 21 - .../src/client/core/ApiRequestOptions.ts | 21 - .../src/client/core/ApiResult.ts | 7 - .../src/client/core/CancelablePromise.ts | 126 ----- .../src/client/core/OpenAPI.ts | 57 -- .../src/client/core/request.ts | 347 ------------- .../node_web_interface/src/client/index.ts | 8 +- .../Common/CreateGenericProcessBotDialog.tsx | 8 +- .../components/Common/ShareFeedbackDialog.tsx | 6 +- .../__tests__/ShareFeedbackDialog.test.tsx | 16 +- .../Debug/DebugTabDeleteControls.tsx | 8 +- .../src/components/Debug/DebugView.tsx | 12 +- .../Debug/dialogs/ExecuteActionDialog.tsx | 29 +- .../Debug/dialogs/PasteProposalDialog.tsx | 22 +- .../Debug/dialogs/SignalAutomationDialog.tsx | 12 +- .../DslKeywords/DslKeywordsView.tsx | 6 +- .../src/components/OctoBots/BotCard.tsx | 2 +- .../src/components/OctoBots/BotGrid.tsx | 2 +- .../components/OctoBots/SelectionToolbar.tsx | 22 +- .../OctoBots/StopAutomationDialog.tsx | 12 +- .../Settings/NodeConfigurationCard.tsx | 2 +- .../WalletManagement/AddWalletDialog.tsx | 27 +- .../WalletManagement/RemoveWalletDialog.tsx | 5 +- .../Settings/WalletManagement/WalletRow.tsx | 10 +- .../Settings/WalletManagementCard.tsx | 3 +- .../src/components/Support/SupportCard.tsx | 2 +- .../src/components/Support/SupportChat.tsx | 13 +- .../components/Tasks/ExportResultsContent.tsx | 9 +- .../Tasks/ImportSteps/EncryptStep.tsx | 20 +- .../src/components/Tasks/ImportTask.tsx | 6 +- .../node_web_interface/src/hooks/useAuth.ts | 10 +- .../src/hooks/useLocalNetworkHostname.ts | 3 +- .../src/hooks/useVpnNetworkHostname.ts | 3 +- .../lib/__tests__/auth-error-messages.test.ts | 124 +---- .../__tests__/configure-api-client.test.ts | 106 ++++ .../__tests__/export-results-fetch.test.ts | 2 +- .../lib/__tests__/feedback-share-api.test.ts | 49 +- .../__tests__/journal-client-event.test.ts | 5 +- .../node_web_interface/src/lib/api-error.ts | 63 +++ .../src/lib/auth-error-messages.ts | 19 +- .../src/lib/configure-api-client.ts | 59 +++ .../accounts-historical-values.test.ts | 63 +++ .../debug/account-historical-values-api.ts | 28 +- ...ggregated-account-historical-values-api.ts | 31 +- .../src/lib/debug/queries.ts | 12 +- .../src/lib/export-results-fetch.ts | 2 +- .../src/lib/feedback-share.ts | 11 +- .../src/lib/hey-api-client-config.ts | 14 + .../src/lib/journal-client-event.ts | 4 +- .../create-generic-process-bot.test.ts | 28 +- .../__tests__/stop-automation.test.ts | 28 +- .../task-matches-search-query.test.ts | 2 +- .../octobots/create-generic-process-bot.ts | 26 +- .../src/lib/octobots/stop-automation.ts | 30 +- .../lib/octobots/task-matches-search-query.ts | 2 +- .../node_web_interface/src/lib/pairing.ts | 8 +- .../src/lib/parse-auth-api-error.ts | 6 +- .../node_web_interface/src/lib/server-keys.ts | 2 +- .../src/lib/task-queries.ts | 11 +- .../src/lib/verify-login-credentials.ts | 33 +- .../node_web_interface/src/routes/__root.tsx | 6 +- .../src/routes/_layout/octobots/index.tsx | 2 +- .../node_web_interface/src/routes/login.tsx | 3 +- .../node_web_interface/src/routes/setup.tsx | 5 +- .../src/routes/setup/index.tsx | 16 +- .../node_web_interface/src/utils.ts | 35 +- .../src/utils/__tests__/task-errors.test.ts | 2 +- .../src/utils/__tests__/task-status.test.ts | 2 +- .../src/utils/task-errors.ts | 2 +- .../src/utils/task-status.ts | 2 +- 76 files changed, 929 insertions(+), 1373 deletions(-) create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/client/index.ts delete mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiError.ts delete mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiRequestOptions.ts delete mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiResult.ts delete mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/CancelablePromise.ts delete mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/OpenAPI.ts delete mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/request.ts create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/configure-api-client.test.ts create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/api-error.ts create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/configure-api-client.ts create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/__tests__/accounts-historical-values.test.ts create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/hey-api-client-config.ts diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/openapi-ts.config.ts b/packages/tentacles/Services/Interfaces/node_web_interface/openapi-ts.config.ts index b5a69e20eb..df3f517586 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/openapi-ts.config.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/openapi-ts.config.ts @@ -5,24 +5,17 @@ export default defineConfig({ output: "./src/client", plugins: [ - "legacy/axios", + { + name: "@hey-api/client-axios", + runtimeConfigPath: "./src/lib/hey-api-client-config", + }, { name: "@hey-api/sdk", - // NOTE: this doesn't allow tree-shaking - asClass: true, - operationId: true, - classNameBuilder: "{{name}}Service", - methodNameBuilder: (operation) => { - // @ts-expect-error - let name: string = operation.name - // @ts-expect-error - const service: string = operation.service - - if (service && name.toLowerCase().startsWith(service.toLowerCase())) { - name = name.slice(service.length) - } - - return name.charAt(0).toLowerCase() + name.slice(1) + operations: { + strategy: "byTags", + containerName: "{{name}}Service", + nesting: "operationId", + methods: "static", }, }, { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/package-lock.json b/packages/tentacles/Services/Interfaces/node_web_interface/package-lock.json index 40efaa68a0..ec5917ea29 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/package-lock.json +++ b/packages/tentacles/Services/Interfaces/node_web_interface/package-lock.json @@ -51,7 +51,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.14", - "@hey-api/openapi-ts": "0.85.0", + "@hey-api/openapi-ts": "0.99.0", "@playwright/test": "1.63.0", "@tanstack/router-plugin": "^1.168.40", "@types/node": "^25.9.8", @@ -1042,70 +1042,116 @@ "license": "MIT" }, "node_modules/@hey-api/codegen-core": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.2.0.tgz", - "integrity": "sha512-c7VjBy/8ed0EVLNgaeS9Xxams1Tuv/WK/b4xXH3Qr4wjzYeJUtxOcoP8YdwNLavqKP8pGiuctjX2Z1Pwc4jMgQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.9.1.tgz", + "integrity": "sha512-s97jL1dgTMuiMHv2BZ1X4Tgd99Mf9GOvGdNqNcGwIMmnR+PgYNoraj4Zvp134MKsNCap/m7k0r0vKKnl56pj4w==", "dev": true, "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "c12": "3.3.4", + "color-support": "1.1.3" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + "node": ">=22.18.0" }, "funding": { "url": "https://github.com/sponsors/hey-api" - }, - "peerDependencies": { - "typescript": ">=5.5.3" } }, "node_modules/@hey-api/json-schema-ref-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.0.tgz", - "integrity": "sha512-BMnIuhVgNmSudadw1GcTsP18Yk5l8FrYrg/OSYNxz0D2E0vf4D5e4j5nUbuY8MU6p1vp7ev0xrfP6A/NWazkzQ==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.4.4.tgz", + "integrity": "sha512-otmd+zCxbYVBIp/mlMTnGkvlNYLkVKgs3VOIq0kSnenhB1+fRwLPQIeSwyWM6E51oXhUedkYjVsVpkVexeuJOA==", "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21" + "@jsdevtools/ono": "7.1.3", + "@types/json-schema": "7.0.15", + "js-yaml": "4.2.0" }, "engines": { - "node": ">= 16" + "node": ">=22.18.0" }, "funding": { "url": "https://github.com/sponsors/hey-api" } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.85.0", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.85.0.tgz", - "integrity": "sha512-LSBHP2/wTF1BnaccHGX1t+0Ss+2VJQxotrLz/0+LK2z8ocuyVZXOYhfBSd7FP8sK78MDJVDBYrPCsBUvNSlH1g==", + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.99.0.tgz", + "integrity": "sha512-SePU/5oEWWkvUBYmvzdYRctseoLuskyhs4ET0RvLIcmzc8yLQoA2R+KtBIQ8bPsoSUB0m4E5SmBnl6aGSA0szQ==", "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "^0.2.0", - "@hey-api/json-schema-ref-parser": "1.2.0", + "@hey-api/codegen-core": "0.9.1", + "@hey-api/json-schema-ref-parser": "1.4.4", + "@hey-api/shared": "0.5.0", + "@hey-api/spec-types": "0.2.0", + "@hey-api/types": "0.1.4", + "@lukeed/ms": "2.0.2", "ansi-colors": "4.1.3", - "c12": "3.3.0", "color-support": "1.1.3", - "commander": "13.0.0", - "handlebars": "4.7.8", - "open": "10.1.2", - "semver": "7.7.2" + "commander": "15.0.0", + "get-tsconfig": "4.14.0" }, "bin": { - "openapi-ts": "bin/index.cjs" + "openapi-ts": "bin/run.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + "node": ">=22.18.0" }, "funding": { "url": "https://github.com/sponsors/hey-api" }, "peerDependencies": { - "typescript": ">=5.5.3" + "typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc" + } + }, + "node_modules/@hey-api/shared": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.5.0.tgz", + "integrity": "sha512-JN/j4Ebh4cJGYIQ5cwWuqe7GeSUyQoz7oC51WqyhKOcrejK6DKZMDkshc5d1eKTRuRL+rjozuRcoUaZZn2DGPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.9.1", + "@hey-api/json-schema-ref-parser": "1.4.4", + "@hey-api/spec-types": "0.2.0", + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "cross-spawn": "7.0.6", + "open": "11.0.0", + "semver": "7.8.4" + }, + "engines": { + "node": ">=22.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/spec-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@hey-api/spec-types/-/spec-types-0.2.0.tgz", + "integrity": "sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" } }, + "node_modules/@hey-api/types": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.4.tgz", + "integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==", + "dev": true, + "license": "MIT" + }, "node_modules/@hookform/resolvers": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", @@ -1313,6 +1359,16 @@ "solid-js": "^1.8.8" } }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@noble/curves": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz", @@ -3995,27 +4051,27 @@ } }, "node_modules/c12": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.0.tgz", - "integrity": "sha512-K9ZkuyeJQeqLEyqldbYLG3wjqwpw4BVaAqvmxq3GYKK0b1A/yYQdIcJxkzAOWcNVWhJpRXAPfZFueekiY/L8Dw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^17.2.2", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.5.1", + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", - "perfect-debounce": "^2.0.0", + "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", - "rc9": "^2.1.2" + "rc9": "^3.0.1" }, "peerDependencies": { - "magicast": "^0.3.5" + "magicast": "*" }, "peerDependenciesMeta": { "magicast": { @@ -4023,36 +4079,6 @@ } } }, - "node_modules/c12/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/c12/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4113,16 +4139,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4167,13 +4183,13 @@ } }, "node_modules/commander": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", - "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/confbox": { @@ -4183,16 +4199,6 @@ "dev": true, "license": "MIT" }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4218,6 +4224,21 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "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/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -4616,20 +4637,25 @@ "node": ">= 0.4" } }, - "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" + "resolve-pkg-maps": "^1.0.0" }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", + "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", + "dev": true, + "license": "MIT", "bin": { "giget": "dist/cli.mjs" } @@ -4661,28 +4687,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -4766,6 +4770,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -4810,6 +4827,13 @@ "node": ">=18" } }, + "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/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4826,9 +4850,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, "funding": [ { @@ -5135,13 +5159,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5212,16 +5229,6 @@ "node": ">= 0.6" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5246,13 +5253,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -5263,13 +5263,6 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.56", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz", @@ -5280,31 +5273,6 @@ "node": ">=18" } }, - "node_modules/nypm": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.10.tgz", - "integrity": "sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.2.2", - "pathe": "^2.0.3", - "tinyexec": "^1.3.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", - "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "dev": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5336,24 +5304,36 @@ "license": "MIT" }, "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -5462,6 +5442,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prettier": { "version": "3.9.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.8.tgz", @@ -5505,14 +5498,14 @@ "license": "MIT" }, "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", + "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", "dev": true, "license": "MIT", "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" + "defu": "^6.1.7", + "destr": "^2.0.5" } }, "node_modules/react": { @@ -5684,6 +5677,16 @@ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", "license": "MIT" }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rolldown": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", @@ -5737,9 +5740,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -5770,6 +5773,29 @@ "seroval": "^1.0" } }, + "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", @@ -5866,16 +5892,6 @@ } } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6013,20 +6029,6 @@ "node": ">=14.17" } }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -6611,6 +6613,22 @@ "dev": true, "license": "MIT" }, + "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", @@ -6628,12 +6646,22 @@ "node": ">=8" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/yallist": { "version": "3.1.1", diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/package.json b/packages/tentacles/Services/Interfaces/node_web_interface/package.json index de59fcb667..ab5254560e 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/package.json +++ b/packages/tentacles/Services/Interfaces/node_web_interface/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.14", - "@hey-api/openapi-ts": "0.85.0", + "@hey-api/openapi-ts": "0.99.0", "@playwright/test": "1.63.0", "@tanstack/router-plugin": "^1.168.40", "@types/node": "^25.9.8", diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/__tests__/utils.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/__tests__/utils.test.ts index e4112a1f4a..db5eee064b 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/__tests__/utils.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/__tests__/utils.test.ts @@ -1,26 +1,16 @@ import { describe, expect, it } from "vitest" -import { ApiError } from "@/client" +import { createTestApiError } from "@/lib/api-error" import { extractErrorMessage } from "@/utils" describe("extractErrorMessage", () => { it("returns message from structured auth error detail", () => { - const err = new ApiError( - { method: "GET", url: "/api/v1/login/test" }, - { - url: "/api/v1/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { - detail: { - code: "auth_invalid_passphrase", - message: "Passphrase verification failed", - }, - }, + const err = createTestApiError(401, { + detail: { + code: "auth_invalid_passphrase", + message: "Passphrase verification failed", }, - "Unauthorized", - ) + }) expect(extractErrorMessage(err)).toBe("Passphrase verification failed") }) }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/bootstrap-app.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/bootstrap-app.tsx index 554af8569d..fca73bd4d5 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/bootstrap-app.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/bootstrap-app.tsx @@ -3,15 +3,11 @@ import { createRouter, RouterProvider } from "@tanstack/react-router" import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { ErrorBoundary } from "react-error-boundary" -import { OpenAPI } from "@/client" import { RecoveryScreen } from "@/components/Common/RecoveryScreen" import { ThemeProvider } from "@/components/theme-provider" import { Toaster } from "@/components/ui/sonner" -import { clearAuth } from "@/hooks/useAuth" import { probeAuthState } from "@/lib/auth-state-probe" -import { shouldRedirectToLoginOn401 } from "@/lib/open-api-401-login-redirect" -import { loadPassword } from "@/lib/device-key" -import { markLoginSessionCleared } from "@/lib/login-session-hint" +import { configureApiClient } from "@/lib/configure-api-client" import type { RecoveryFailureKind } from "@/components/Common/RecoveryScreen" import { isWebCryptoAvailable } from "@/lib/secure-context" import { @@ -21,38 +17,6 @@ import { } from "@/lib/shell-error-reporting" import { routeTree } from "@/routeTree.gen" -// Wire generated API client credentials and session recovery before any route loads. -export function configureOpenApi(): void { - OpenAPI.BASE = - import.meta.env.NODE_API_URL || - (import.meta.env.DEV ? "http://localhost:8000" : "") - OpenAPI.USERNAME = async () => { - return localStorage.getItem("auth_username") || "" - } - OpenAPI.PASSWORD = async () => { - return (await loadPassword()) ?? "" - } - - let isRedirectingOnAuthFailure = false - // On 401 for a protected page, clear local session and hard-navigate to login so the UI - // never keeps calling APIs with a stale username and no usable password. - OpenAPI.interceptors.response.use((response) => { - if ( - shouldRedirectToLoginOn401(response, { - pathname: window.location.pathname, - isRedirectingOnAuthFailure, - }) - ) { - isRedirectingOnAuthFailure = true - markLoginSessionCleared() - void clearAuth().finally(() => { - window.location.href = "/app/login" - }) - } - return response - }) -} - function createAppRouter() { return createRouter({ routeTree, @@ -127,7 +91,7 @@ function renderApp(rootElement: HTMLElement): void { // Startup: configure API → mount root → require Web Crypto → auth probe → app or RecoveryScreen. export async function bootstrapApp(): Promise { - configureOpenApi() + configureApiClient() const rootElement = document.getElementById("root") if (!rootElement) { throw new Error("Root element not found") diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/client/index.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/client/index.ts new file mode 100644 index 0000000000..3e877a5426 --- /dev/null +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export type { ServerSentEventsResult } from '../core/serverSentEvents.gen'; +export type { ClientMeta } from '../core/types.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + TDataShape, +} from './types.gen'; +export { createConfig } from './utils.gen'; diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiError.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiError.ts deleted file mode 100644 index 36675d288a..0000000000 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiError.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: unknown; - public readonly request: ApiRequestOptions; - - constructor(request: ApiRequestOptions, response: ApiResult, message: string) { - super(message); - - this.name = 'ApiError'; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} \ No newline at end of file diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiRequestOptions.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiRequestOptions.ts deleted file mode 100644 index 939a0aa4c8..0000000000 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiRequestOptions.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type ApiRequestOptions = { - readonly body?: any; - readonly cookies?: Record; - readonly errors?: Record; - readonly formData?: Record | any[] | Blob | File; - readonly headers?: Record; - readonly mediaType?: string; - readonly method: - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT'; - readonly path?: Record; - readonly query?: Record; - readonly responseHeader?: string; - readonly responseTransformer?: (data: unknown) => Promise; - readonly url: string; -}; \ No newline at end of file diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiResult.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiResult.ts deleted file mode 100644 index 4c58e39138..0000000000 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/ApiResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type ApiResult = { - readonly body: TData; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly url: string; -}; \ No newline at end of file diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/CancelablePromise.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/CancelablePromise.ts deleted file mode 100644 index d95c755e0a..0000000000 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/CancelablePromise.ts +++ /dev/null @@ -1,126 +0,0 @@ -export class CancelError extends Error { - constructor(message: string) { - super(message); - this.name = 'CancelError'; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise implements Promise { - private _isResolved: boolean; - private _isRejected: boolean; - private _isCancelled: boolean; - readonly cancelHandlers: (() => void)[]; - readonly promise: Promise; - private _resolve?: (value: T | PromiseLike) => void; - private _reject?: (reason?: unknown) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: unknown) => void, - onCancel: OnCancel - ) => void - ) { - this._isResolved = false; - this._isRejected = false; - this._isCancelled = false; - this.cancelHandlers = []; - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - - const onResolve = (value: T | PromiseLike): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isResolved = true; - if (this._resolve) this._resolve(value); - }; - - const onReject = (reason?: unknown): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isRejected = true; - if (this._reject) this._reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this.cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, 'isResolved', { - get: (): boolean => this._isResolved, - }); - - Object.defineProperty(onCancel, 'isRejected', { - get: (): boolean => this._isRejected, - }); - - Object.defineProperty(onCancel, 'isCancelled', { - get: (): boolean => this._isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag](): string { - return "Cancellable Promise"; - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: unknown) => TResult2 | PromiseLike) | null - ): Promise { - return this.promise.then(onFulfilled, onRejected); - } - - public catch( - onRejected?: ((reason: unknown) => TResult | PromiseLike) | null - ): Promise { - return this.promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.promise.finally(onFinally); - } - - public cancel(): void { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isCancelled = true; - if (this.cancelHandlers.length) { - try { - for (const cancelHandler of this.cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn('Cancellation threw an error', error); - return; - } - } - this.cancelHandlers.length = 0; - if (this._reject) this._reject(new CancelError('Request aborted')); - } - - public get isCancelled(): boolean { - return this._isCancelled; - } -} \ No newline at end of file diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/OpenAPI.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/OpenAPI.ts deleted file mode 100644 index 74f92b4085..0000000000 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/OpenAPI.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { AxiosRequestConfig, AxiosResponse } from 'axios'; -import type { ApiRequestOptions } from './ApiRequestOptions'; - -type Headers = Record; -type Middleware = (value: T) => T | Promise; -type Resolver = (options: ApiRequestOptions) => Promise; - -export class Interceptors { - _fns: Middleware[]; - - constructor() { - this._fns = []; - } - - eject(fn: Middleware): void { - const index = this._fns.indexOf(fn); - if (index !== -1) { - this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]; - } - } - - use(fn: Middleware): void { - this._fns = [...this._fns, fn]; - } -} - -export type OpenAPIConfig = { - BASE: string; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - ENCODE_PATH?: ((path: string) => string) | undefined; - HEADERS?: Headers | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - VERSION: string; - WITH_CREDENTIALS: boolean; - interceptors: { - request: Interceptors; - response: Interceptors; - }; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: '', - CREDENTIALS: 'include', - ENCODE_PATH: undefined, - HEADERS: undefined, - PASSWORD: undefined, - TOKEN: undefined, - USERNAME: undefined, - VERSION: '0.1.0', - WITH_CREDENTIALS: false, - interceptors: { - request: new Interceptors(), - response: new Interceptors(), - }, -}; \ No newline at end of file diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/request.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/request.ts deleted file mode 100644 index ecc2e393cd..0000000000 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/core/request.ts +++ /dev/null @@ -1,347 +0,0 @@ -import axios from 'axios'; -import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; - -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isString = (value: unknown): value is string => { - return typeof value === 'string'; -}; - -export const isStringWithValue = (value: unknown): value is string => { - return isString(value) && value !== ''; -}; - -export const isBlob = (value: any): value is Blob => { - return value instanceof Blob; -}; - -export const isFormData = (value: unknown): value is FormData => { - return value instanceof FormData; -}; - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } -}; - -export const getQueryString = (params: Record): string => { - const qs: string[] = []; - - const append = (key: string, value: unknown) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const encodePair = (key: string, value: unknown) => { - if (value === undefined || value === null) { - return; - } - - if (value instanceof Date) { - append(key, value.toISOString()); - } else if (Array.isArray(value)) { - value.forEach(v => encodePair(key, v)); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v)); - } else { - append(key, value); - } - }; - - Object.entries(params).forEach(([key, value]) => encodePair(key, value)); - - return qs.length ? `?${qs.join('&')}` : ''; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = config.BASE + path; - return options.query ? url + getQueryString(options.query) : url; -}; - -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: unknown) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([, value]) => value !== undefined && value !== null) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver = (options: ApiRequestOptions) => Promise; - -export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { - if (typeof resolver === 'function') { - return (resolver as Resolver)(options); - } - return resolver; -}; - -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - // @ts-ignore - resolve(options, config.TOKEN), - // @ts-ignore - resolve(options, config.USERNAME), - // @ts-ignore - resolve(options, config.PASSWORD), - // @ts-ignore - resolve(options, config.HEADERS), - ]); - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - }) - .filter(([, value]) => value !== undefined && value !== null) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } else if (options.formData !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } - } - - return headers; -}; - -export const getRequestBody = (options: ApiRequestOptions): unknown => { - if (options.body) { - return options.body; - } - return undefined; -}; - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: unknown, - formData: FormData | undefined, - headers: Record, - onCancel: OnCancel, - axiosClient: AxiosInstance -): Promise> => { - const controller = new AbortController(); - - let requestConfig: AxiosRequestConfig = { - data: body ?? formData, - headers, - method: options.method, - signal: controller.signal, - url, - withCredentials: config.WITH_CREDENTIALS, - }; - - onCancel(() => controller.abort()); - - for (const fn of config.interceptors.request._fns) { - requestConfig = await fn(requestConfig); - } - - try { - return await axiosClient.request(requestConfig); - } catch (error) { - const axiosError = error as AxiosError; - if (axiosError.response) { - return axiosError.response; - } - throw error; - } -}; - -export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader]; - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = (response: AxiosResponse): unknown => { - if (response.status !== 204) { - return response.data; - } - return undefined; -}; - -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record = { - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Payload Too Large', - 414: 'URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Range Not Satisfiable', - 417: 'Expectation Failed', - 418: 'Im a teapot', - 421: 'Misdirected Request', - 422: 'Unprocessable Content', - 423: 'Locked', - 424: 'Failed Dependency', - 425: 'Too Early', - 426: 'Upgrade Required', - 428: 'Precondition Required', - 429: 'Too Many Requests', - 431: 'Request Header Fields Too Large', - 451: 'Unavailable For Legal Reasons', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported', - 506: 'Variant Also Negotiates', - 507: 'Insufficient Storage', - 508: 'Loop Detected', - 510: 'Not Extended', - 511: 'Network Authentication Required', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise - * @throws ApiError - */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options); - - if (!onCancel.isCancelled) { - let response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); - - for (const fn of config.interceptors.response._fns) { - response = await fn(response); - } - - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - let transformedBody = responseBody; - if (options.responseTransformer && isSuccess(response.status)) { - transformedBody = await options.responseTransformer(responseBody) - } - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? transformedBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; \ No newline at end of file diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/index.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/index.ts index 50a1dd734c..3c7a5abb8b 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/client/index.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/client/index.ts @@ -1,6 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI'; -export * from './sdk.gen'; -export * from './types.gen'; \ No newline at end of file + +export { AccountsService, ConfigService, DebugService, DefaultService, DslService, ExchangesService, FeedbackService, JournalService, LoginService, LogsService, MarketMakingService, NodesService, OctobotsService, type Options, SetupService, TasksService, UsersService, WalletsService } from './sdk.gen'; +export type { Account, AccountActionResult, AccountActionResultErrorMessage, AccountAuthActionResult, AccountAuthActionResultErrorMessage, AccountAuthentication, AccountPermission, AccountReference, AccountsGetAccountHistoricalValuesData, AccountsGetAccountHistoricalValuesError, AccountsGetAccountHistoricalValuesErrors, AccountsGetAccountHistoricalValuesResponses, AccountsGetAggregatedAccountHistoricalValuesData, AccountsGetAggregatedAccountHistoricalValuesError, AccountsGetAggregatedAccountHistoricalValuesErrors, AccountsGetAggregatedAccountHistoricalValuesResponses, AccountSpecifics, AccountState, AccountStatus, AccountStatusMessage, AccountTrading, AccountTradingWithAccountId, AccountType, Action, ActionConfigurationType, ActiveOrderSwapStrategy, ActiveOrderSwapStrategyType, AssetAccount, AuthErrorCode, AutomationActionResult, AutomationActionResultErrorMessage, AutomationConfiguration, AutomationMetadata, AutomationSignalType, AutomationState, BankAccount, BlockchainAccount, BrokerAccount, CancelPolicy, CancelPolicyType, CancelTasksBody, ChildOctoBotProcessState, ClientOptions, ConfigGetOctochatConfigData, ConfigGetOctochatConfigResponses, ConfigGetOctochatIdentityData, ConfigGetOctochatIdentityResponses, CopyConfiguration, CreateAccountAuthConfiguration, CreateAccountConfiguration, CreateAutomationConfiguration, CreateExchangeConfigConfiguration, CreateGenericProcessBotRequest, CreateGenericProcessBotResponse, CreateStrategyConfiguration, CreateWalletBody, Debug, DebugExecuteUserActionData, DebugExecuteUserActionError, DebugExecuteUserActionErrors, DebugExecuteUserActionResponse, DebugExecuteUserActionResponses, DebugGetDebugData, DebugGetDebugError, DebugGetDebugErrors, DebugGetDebugResponse, DebugGetDebugResponses, DebugState, DeleteAccountAuthConfiguration, DeleteAccountConfiguration, DeleteExchangeConfigConfiguration, DeleteStrategyConfiguration, DetailedAsset, DetailedAssetsForTradingType, DslGetDslKeywordsData, DslGetDslKeywordsResponse, DslGetDslKeywordsResponses, DslKeyword, DslKeywordCategory, DslKeywordsState, DslParameter, DslParameterDefaultValue, DslParameterOption, DslValueType, EditAccountAuthConfiguration, EditAccountConfiguration, EditAutomationConfiguration, EditExchangeConfigConfiguration, EditStrategyConfiguration, EvaluatorConfiguration, ExchangeAccount, ExchangeAvailability, ExchangeConfig, ExchangeConfigActionResult, ExchangeConfigActionResultErrorMessage, ExchangesGetDexPairsData, ExchangesGetDexPairsError, ExchangesGetDexPairsErrors, ExchangesGetDexPairsResponses, ExchangesGetExchangesAvailabilityData, ExchangesGetExchangesAvailabilityResponse, ExchangesGetExchangesAvailabilityResponses, ExchangesGetTradedPairsAndTimeframesData, ExchangesGetTradedPairsAndTimeframesError, ExchangesGetTradedPairsAndTimeframesErrors, ExchangesGetTradedPairsAndTimeframesResponses, ExchangesGetTradedPairsData, ExchangesGetTradedPairsError, ExchangesGetTradedPairsErrors, ExchangesGetTradedPairsResponse, ExchangesGetTradedPairsResponses, ExchangeSupportStatus, Execution, ExportLogsRequest, ExportResultsBody, Fee, FeedbackExportFeedbackData, FeedbackExportFeedbackError, FeedbackExportFeedbackErrors, FeedbackExportFeedbackResponse, FeedbackExportFeedbackResponses, FeedbackGetFeedbackPreviewData, FeedbackGetFeedbackPreviewResponse, FeedbackGetFeedbackPreviewResponses, FeedbackPreviewResponse, FeedbackUploadEnvelope, FeedbackUploadRequest, GenericAccount, GenericProcessConfiguration, GenericWorkflowConfiguration, HttpValidationError, JournalClientEventRequest, JournalClientEventResponse, JournalRecordJournalClientEventData, JournalRecordJournalClientEventError, JournalRecordJournalClientEventErrors, JournalRecordJournalClientEventResponse, JournalRecordJournalClientEventResponses, LocalNetworkAddress, LoginTestAuthData, LoginTestAuthError, LoginTestAuthErrors, LoginTestAuthResponse, LoginTestAuthResponses, LogsExportLogsData, LogsExportLogsError, LogsExportLogsErrors, LogsExportLogsResponses, MarketMakingConfiguration, MarketMakingFundsDistribution, MarketMakingHedgingEngine, MarketMakingMarketMakingPingData, MarketMakingMarketMakingPingResponses, MarketMakingMarketMakingRootData, MarketMakingMarketMakingRootError, MarketMakingMarketMakingRootErrors, MarketMakingMarketMakingRootResponses, MarketMakingOrderBookDepth, MarketMakingOrdersDistribution, MarketMakingReferencePair, MarketMakingScheduledVolume, MarketMakingStopConditions, MarketMakingSymbolConfiguration, Node, NodeAuthErrorDetail, NodeConfigUpdate, NodesGetCurrentNodeData, NodesGetCurrentNodeResponse, NodesGetCurrentNodeResponses, NodesGetNodeConfigData, NodesGetNodeConfigResponses, NodesStopNodeData, NodesStopNodeResponse, NodesStopNodeResponses, NodesUpdateNodeConfigData, NodesUpdateNodeConfigError, NodesUpdateNodeConfigErrors, NodesUpdateNodeConfigResponses, OctobotsCreateGenericProcessBotData, OctobotsCreateGenericProcessBotError, OctobotsCreateGenericProcessBotErrors, OctobotsCreateGenericProcessBotResponse, OctobotsCreateGenericProcessBotResponses, Order, OrderGroup, OrderGroupType, OrderStatus, OrderSummary, OrderType, Position, PositionStatus, PositionSummary, RefreshAccountsConfiguration, ResetAccountTradingDataConfiguration, RestartAutomationConfiguration, ServeSpaAppData, ServeSpaAppError, ServeSpaAppErrors, ServeSpaAppResponses, ServeSpaAppRootData, ServeSpaAppRootResponses, SetupExportWalletData, SetupExportWalletError, SetupExportWalletErrors, SetupExportWalletResponse, SetupExportWalletResponses, SetupGetLocalNetworkAddressData, SetupGetLocalNetworkAddressResponse, SetupGetLocalNetworkAddressResponses, SetupGetSetupStatusData, SetupGetSetupStatusResponse, SetupGetSetupStatusResponses, SetupGetVpnNetworkAddressData, SetupGetVpnNetworkAddressResponse, SetupGetVpnNetworkAddressResponses, SetupInit, SetupInitSetupData, SetupInitSetupError, SetupInitSetupErrors, SetupInitSetupResponse, SetupInitSetupResponses, SetupResult, SetupStatus, Side, SignalAutomationConfiguration, SignalAutomationConfigurationSignalPayload, SignalBotConfiguration, SignalPriorityActionExecutionResult, StopAutomationConfiguration, Strategy, StrategyActionResult, StrategyActionResultErrorMessage, StrategyConfiguration, StrategyEvaluatorConfiguration, StrategyReference, TaskInput, TaskMetadata, TaskOutput, TasksCancelTasksData, TasksCancelTasksError, TasksCancelTasksErrors, TasksCancelTasksResponse, TasksCancelTasksResponses, TasksCreateTasksData, TasksCreateTasksError, TasksCreateTasksErrors, TasksCreateTasksResponse, TasksCreateTasksResponses, TasksDeleteTasksData, TasksDeleteTasksError, TasksDeleteTasksErrors, TasksDeleteTasksResponse, TasksDeleteTasksResponses, TasksExportResultsData, TasksExportResultsError, TasksExportResultsErrors, TasksExportResultsResponse, TasksExportResultsResponses, TasksGetMetricsData, TasksGetMetricsResponses, TasksGetServerPublicKeysData, TasksGetServerPublicKeysResponse, TasksGetServerPublicKeysResponses, TasksGetTasksData, TasksGetTasksError, TasksGetTasksErrors, TasksGetTasksResponse, TasksGetTasksResponses, TaskStatus, TasksUpdateTaskData, TasksUpdateTaskError, TasksUpdateTaskErrors, TasksUpdateTaskResponse, TasksUpdateTaskResponses, TimeFrame, Trade, TradedPairsByExchange, TradeSummary, TradingTentaclesConfiguration, TradingType, TrailingProfile, TrailingProfileType, Transaction, TransactionType, UpdateHistoricalExchangesDataConfiguration, UpdateWalletBody, User, UserAction, UserActionConfiguration, UserActionResult, UserActionResultType, UserActionStatus, UserActionType, UsersReadUserMeData, UsersReadUserMeResponse, UsersReadUserMeResponses, ValidationError, VpnNetworkAddress, WalletExport, WalletInfo, WalletsCreateWalletData, WalletsCreateWalletError, WalletsCreateWalletErrors, WalletsCreateWalletResponse, WalletsCreateWalletResponses, WalletsDeleteWalletData, WalletsDeleteWalletError, WalletsDeleteWalletErrors, WalletsDeleteWalletResponse, WalletsDeleteWalletResponses, WalletsListWalletsData, WalletsListWalletsResponse, WalletsListWalletsResponses, WalletsUpdateWalletData, WalletsUpdateWalletError, WalletsUpdateWalletErrors, WalletsUpdateWalletResponse, WalletsUpdateWalletResponses, WorkflowStatus } from './types.gen'; diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/CreateGenericProcessBotDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/CreateGenericProcessBotDialog.tsx index e29aabfda1..8de64e8541 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/CreateGenericProcessBotDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/CreateGenericProcessBotDialog.tsx @@ -45,10 +45,10 @@ export function CreateGenericProcessBotDialog({ >(null) const createMutation = useMutation({ - mutationFn: (trimmedName: string) => - OctobotsService.createGenericProcessBot({ - requestBody: buildCreateGenericProcessBotRequestBody(trimmedName), - }), + mutationFn: async (trimmedName: string) => + (await OctobotsService.octobotsCreateGenericProcessBot({ + body: buildCreateGenericProcessBotRequestBody(trimmedName), + })).data, onSuccess: (_result, trimmedName) => { setCreatedBotName(trimmedName) setStep("success") diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/ShareFeedbackDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/ShareFeedbackDialog.tsx index 9acad19c4a..e86a29dbbb 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/ShareFeedbackDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/ShareFeedbackDialog.tsx @@ -2,7 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query" import { Link } from "@tanstack/react-router" import { useEffect, useState } from "react" -import { ApiError } from "@/client" +import { getApiErrorStatus, isApiError } from "@/lib/api-error" import { AuthLoginRedirectSuppressionProvider } from "@/components/Common/AuthLoginRedirectSuppressionProvider" import { Button } from "@/components/ui/button" import { @@ -80,7 +80,7 @@ function getContextChipLabel(context: ShareFeedbackContext): string | null { } function isAuthRequiredError(error: unknown): boolean { - return error instanceof ApiError && error.status === 401 + return isApiError(error) && getApiErrorStatus(error) === 401 } type ActivityHistorySectionProps = { @@ -167,7 +167,7 @@ export function ShareFeedbackDialogContent({ const showSignInPrompt = previewAuthBlocked && !isRecoveryContext const submitMutation = useMutation({ - mutationFn: () => + mutationFn: async () => submitFeedbackDownload({ note, context, diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/__tests__/ShareFeedbackDialog.test.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/__tests__/ShareFeedbackDialog.test.tsx index 1234937ea9..ec622251b1 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/__tests__/ShareFeedbackDialog.test.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Common/__tests__/ShareFeedbackDialog.test.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { renderToStaticMarkup } from "react-dom/server" import { beforeEach, describe, expect, it, vi } from "vitest" -import { ApiError } from "@/client" +import { createTestApiError } from "@/lib/api-error" import { ShareFeedbackDialogContent } from "@/components/Common/ShareFeedbackDialog" import { Dialog } from "@/components/ui/dialog" @@ -107,18 +107,8 @@ function createPreviewResponse(eventCount: number) { } } -function create401PreviewError(): ApiError { - return new ApiError( - { method: "GET", url: "/api/v1/feedback/preview" }, - { - url: "/api/v1/feedback/preview", - ok: false, - status: 401, - statusText: "Unauthorized", - body: {}, - }, - "Unauthorized", - ) +function create401PreviewError() { + return createTestApiError(401, {}) } function renderDialog( diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugTabDeleteControls.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugTabDeleteControls.tsx index 5c5658248a..7e12c073ce 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugTabDeleteControls.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugTabDeleteControls.tsx @@ -39,10 +39,10 @@ export function DebugTabDeleteControls({ const { showSuccessToast, showErrorToast } = useCustomToast() const deleteMutation = useMutation({ - mutationFn: () => - TasksService.deleteTasks({ - taskIds: Array.from(selectedIds), - }), + mutationFn: async () => + (await TasksService.tasksDeleteTasks({ + query: { taskIds: Array.from(selectedIds) }, + })).data, onSuccess: () => { showSuccessToast( `Deleted ${selectedCount} item${selectedCount !== 1 ? "s" : ""}`, diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugView.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugView.tsx index d052bd43ea..e1ce3f732f 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugView.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/DebugView.tsx @@ -3,7 +3,8 @@ import { Link } from "@tanstack/react-router" import { Bug, Code, ScrollText } from "lucide-react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import { type ApiError, type DebugState, WalletsService } from "@/client" +import { type DebugState, WalletsService } from "@/client" +import { getApiErrorStatus, type ApiError } from "@/lib/api-error" import { DebugExecuteActionDialogHost, type DebugExecuteActionHandle, @@ -66,7 +67,8 @@ export function DebugView() { const { data: wallets = [], isPending: isWalletsLoading } = useQuery({ queryKey: ["wallets"], - queryFn: () => WalletsService.listWallets(), + queryFn: async () => + (await WalletsService.walletsListWallets()).data, enabled: isSuperuser, }) @@ -76,7 +78,7 @@ export function DebugView() { ...getDebugQueryOptions(walletQueryParam), enabled: !isImportedMode, retry: (failureCount, error) => { - const status = (error as ApiError)?.status + const status = getApiErrorStatus(error as ApiError) if (status === 503) return false return failureCount < 2 }, @@ -87,7 +89,7 @@ export function DebugView() { !isImportedMode && debugQuery.isError && debugQuery.error && - (debugQuery.error as ApiError).status !== 503 + getApiErrorStatus(debugQuery.error as ApiError) !== 503 ) { handleError.bind(showErrorToast)(debugQuery.error as ApiError) } @@ -104,7 +106,7 @@ export function DebugView() { const schedulerUnavailable = !isImportedMode && debugQuery.isError && - (debugQuery.error as ApiError)?.status === 503 + getApiErrorStatus(debugQuery.error as ApiError) === 503 const importedSummary = useMemo(() => { if (!importedSnapshot || !importMeta) return null diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/ExecuteActionDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/ExecuteActionDialog.tsx index d631940495..3c70287f03 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/ExecuteActionDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/ExecuteActionDialog.tsx @@ -2,7 +2,8 @@ import { useMutation } from "@tanstack/react-query" import { Copy, TriangleAlert } from "lucide-react" import { useEffect, useMemo, useState } from "react" -import { type ApiError, DebugService, type UserAction } from "@/client" +import { DebugService, type UserAction } from "@/client" +import { formatApiErrorDetailMessage, type ApiError } from "@/lib/api-error" import { Button } from "@/components/ui/button" import { Dialog, @@ -32,18 +33,6 @@ import { userActionTemplateKeyFromActionType, } from "@/lib/debug/user-action-templates" -function getApiErrorMessage(error: ApiError): string { - const errorDetail = (error.body as { detail?: unknown } | undefined)?.detail - if (typeof errorDetail === "string" && errorDetail.length > 0) { - return errorDetail - } - if (Array.isArray(errorDetail) && errorDetail.length > 0) { - const firstDetail = errorDetail[0] as { msg?: string } - if (firstDetail.msg) return firstDetail.msg - } - return error.message || "Something went wrong." -} - function ExecuteActionDialogError({ message }: { message: string }) { return (
- DebugService.executeUserAction({ - requestBody: body, - walletAddress: walletAddress ?? null, - }), + mutationFn: async (body: UserAction) => + (await DebugService.debugExecuteUserAction({ + body, + query: { wallet_address: walletAddress ?? null }, + })).data, onSuccess: () => { showSuccessToast("Action submitted") onOpenChange(false) onSuccess() }, onError: (error) => { - setSubmitError(getApiErrorMessage(error as ApiError)) + setSubmitError( + formatApiErrorDetailMessage(error, "Something went wrong."), + ) }, }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/PasteProposalDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/PasteProposalDialog.tsx index 499b911900..7766ede1a0 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/PasteProposalDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/PasteProposalDialog.tsx @@ -5,7 +5,8 @@ import { } from "@drakkar.software/octobot-client/protocol" import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import { type ApiError, DebugService } from "@/client" +import { type DebugState, DebugService } from "@/client" +import type { ApiError } from "@/lib/api-error" import { Button } from "@/components/ui/button" import { Dialog, @@ -113,9 +114,9 @@ export function PasteProposalDialog({ configuration: ProposedActionEntry["configuration"] }) => { try { - await DebugService.executeUserAction({ - requestBody: { id, configuration }, - walletAddress: walletAddress ?? null, + await DebugService.debugExecuteUserAction({ + body: { id, configuration }, + query: { wallet_address: walletAddress ?? null }, }) } catch (error) { throw new Error(describeSubmitError(error)) @@ -124,10 +125,15 @@ export function PasteProposalDialog({ [walletAddress], ) - const fetchDebugState = useCallback( - () => DebugService.getDebug(walletAddress ? { walletAddress } : {}), - [walletAddress], - ) + const fetchDebugState = useCallback(async (): Promise => { + const response = await DebugService.debugGetDebug({ + ...(walletAddress + ? { query: { wallet_address: walletAddress } } + : {}), + throwOnError: true, + }) + return response.data + }, [walletAddress]) const handleRun = (proposal: ActionProposal) => { setSteps(proposal.actions.map(() => ({ state: "waiting" }))) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/SignalAutomationDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/SignalAutomationDialog.tsx index 51c6519bec..d2a3ec929b 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/SignalAutomationDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Debug/dialogs/SignalAutomationDialog.tsx @@ -2,12 +2,12 @@ import { useMutation } from "@tanstack/react-query" import { useEffect, useState } from "react" import { - type ApiError, type AutomationSignalType, type AutomationState, DebugService, type UserAction, } from "@/client" +import type { ApiError } from "@/lib/api-error" import { Button } from "@/components/ui/button" import { Dialog, @@ -82,11 +82,11 @@ export function SignalAutomationDialog({ }, [open]) const mutation = useMutation({ - mutationFn: (body: UserAction) => - DebugService.executeUserAction({ - requestBody: body, - walletAddress: walletAddress ?? null, - }), + mutationFn: async (body: UserAction) => + (await DebugService.debugExecuteUserAction({ + body, + query: { wallet_address: walletAddress ?? null }, + })).data, onSuccess: () => { showSuccessToast("Signal submitted") onOpenChange(false) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/DslKeywords/DslKeywordsView.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/DslKeywords/DslKeywordsView.tsx index f4a6c11b74..4c63bc9e53 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/DslKeywords/DslKeywordsView.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/DslKeywords/DslKeywordsView.tsx @@ -2,7 +2,8 @@ import { useQuery } from "@tanstack/react-query" import { BookOpen } from "lucide-react" import { useEffect } from "react" -import { type ApiError, DslService } from "@/client" +import { DslService } from "@/client" +import type { ApiError } from "@/lib/api-error" import { DslKeywordsTable } from "@/components/DslKeywords/DslKeywordsTable" import { Card, @@ -19,7 +20,8 @@ export function DslKeywordsView() { const keywordsQuery = useQuery({ queryKey: ["dsl-keywords"], - queryFn: () => DslService.getDslKeywords(), + queryFn: async () => + (await DslService.dslGetDslKeywords()).data, }) useEffect(() => { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotCard.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotCard.tsx index 558f7a4365..d94ada23ea 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotCard.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotCard.tsx @@ -3,7 +3,7 @@ import { memo } from "react" import type { ChildOctoBotProcessState, - Task_Output as Task, + TaskOutput as Task, TaskStatus, } from "@/client" import { Badge } from "@/components/ui/badge" diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotGrid.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotGrid.tsx index a80c5b8902..a955f851f5 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotGrid.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/BotGrid.tsx @@ -1,7 +1,7 @@ import { Link } from "@tanstack/react-router" import { Bot, Plus } from "lucide-react" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { Button } from "@/components/ui/button" import { BotCard } from "./BotCard" diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/SelectionToolbar.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/SelectionToolbar.tsx index dfe14b8dd1..9ec20ed5aa 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/SelectionToolbar.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/SelectionToolbar.tsx @@ -3,7 +3,7 @@ import { Link, useNavigate } from "@tanstack/react-router" import { Ban, ScrollText, Square, Trash2 } from "lucide-react" import { useMemo, useState } from "react" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { TasksService } from "@/client" import { StopAutomationDialog } from "@/components/OctoBots/StopAutomationDialog" import { Button } from "@/components/ui/button" @@ -92,10 +92,12 @@ export function SelectionToolbar({ ) const deleteMutation = useMutation({ - mutationFn: () => - TasksService.deleteTasks({ - taskIds: inactiveTasks.map((task) => task.id as string), - }), + mutationFn: async () => + (await TasksService.tasksDeleteTasks({ + query: { + taskIds: inactiveTasks.map((task) => task.id as string), + }, + })).data, onSuccess: () => { showSuccessToast( `Deleted ${inactiveTasks.length} OctoBot${inactiveTasks.length !== 1 ? "s" : ""}`, @@ -110,10 +112,10 @@ export function SelectionToolbar({ }) const cancelMutation = useMutation({ - mutationFn: () => - TasksService.cancelTasks({ - requestBody: { task_ids: activeTasks.map((task) => task.id as string) }, - }), + mutationFn: async () => + (await TasksService.tasksCancelTasks({ + body: { task_ids: activeTasks.map((task) => task.id as string) }, + })).data, onSuccess: () => { showSuccessToast( `Cancelled ${activeTasks.length} OctoBot${activeTasks.length !== 1 ? "s" : ""}`, @@ -127,7 +129,7 @@ export function SelectionToolbar({ }) const shareLogsMutation = useMutation({ - mutationFn: () => shareWorkflowLogs(Array.from(selectedIds)), + mutationFn: async () => shareWorkflowLogs(Array.from(selectedIds)), onSuccess: (res) => { if (res.status === "shared") { showSuccessToast("Workflow logs shared to your support ticket") diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/StopAutomationDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/StopAutomationDialog.tsx index a0ce2007ca..a72a3708e0 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/StopAutomationDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/OctoBots/StopAutomationDialog.tsx @@ -2,7 +2,7 @@ import { TriangleAlert } from "lucide-react" import { useEffect, useMemo, useState } from "react" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { DebugService } from "@/client" import { Button } from "@/components/ui/button" import { @@ -49,10 +49,10 @@ async function stopAutomationsSequentially( const failed: { automationId: string; message: string }[] = [] for (const automationId of automationIds) { try { - await DebugService.executeUserAction({ - requestBody: buildStopAutomationUserAction(automationId), - walletAddress: null, - }) + (await DebugService.debugExecuteUserAction({ + body: buildStopAutomationUserAction(automationId), + query: { wallet_address: null }, + })).data } catch (error) { failed.push({ automationId, @@ -97,7 +97,7 @@ export function StopAutomationDialog({ }, [open, stoppableTasks.map((task) => task.id).join(",")]) const stopMutation = useMutation({ - mutationFn: (automationIds: string[]) => + mutationFn: async (automationIds: string[]) => stopAutomationsSequentially(automationIds), onSuccess: (result, automationIds) => { if (result.failed.length > 0) { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/NodeConfigurationCard.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/NodeConfigurationCard.tsx index a699f242c7..a6bb6e06a7 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/NodeConfigurationCard.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/NodeConfigurationCard.tsx @@ -45,7 +45,7 @@ export function NodeConfigurationCard() { const timerRef = useRef | null>(null) const stopMutation = useMutation({ - mutationFn: () => NodesService.stopNode(), + mutationFn: async () => (await NodesService.nodesStopNode()).data, onSuccess: () => { setStopDialogOpen(false) showSuccessToast("OctoBot is stopping") diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/AddWalletDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/AddWalletDialog.tsx index 4b1e55fc5d..dd337810d4 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/AddWalletDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/AddWalletDialog.tsx @@ -1,7 +1,8 @@ import { useMutation } from "@tanstack/react-query" import { Plus, TriangleAlert } from "lucide-react" import { useState } from "react" -import { type ApiError, WalletsService } from "@/client" +import { WalletsService } from "@/client" +import { getApiErrorStatus, type ApiError } from "@/lib/api-error" import { extractErrorMessage } from "@/utils" import { Dialog, @@ -36,15 +37,19 @@ export function AddWalletDialog({ onSuccess }: { onSuccess: () => void }) { } const mutation = useMutation({ - mutationFn: () => - WalletsService.createWallet({ - requestBody: { - passphrase, - name: name.trim() || null, - private_key: importMode && !importBySeed && privateKey.trim() ? privateKey.trim() : null, - seed: importMode && importBySeed && seed.trim() ? seed.trim() : null, - }, - }), + mutationFn: async () => + (await WalletsService.walletsCreateWallet({ + body: { + passphrase, + name: name.trim() || null, + private_key: + importMode && !importBySeed && privateKey.trim() + ? privateKey.trim() + : null, + seed: + importMode && importBySeed && seed.trim() ? seed.trim() : null, + }, + })).data, onSuccess: () => { setOpen(false) reset() @@ -52,7 +57,7 @@ export function AddWalletDialog({ onSuccess }: { onSuccess: () => void }) { }, onError: (e: unknown) => { const err = e as ApiError - if (err?.status === 409) { + if (getApiErrorStatus(err) === 409) { setError("This wallet is already imported.") return } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/RemoveWalletDialog.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/RemoveWalletDialog.tsx index bfaa3cf8bc..00221e76a3 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/RemoveWalletDialog.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/RemoveWalletDialog.tsx @@ -22,7 +22,10 @@ export function RemoveWalletDialog({ const [open, setOpen] = useState(false) const mutation = useMutation({ - mutationFn: () => WalletsService.deleteWallet({ address: wallet.address }), + mutationFn: async () => + (await WalletsService.walletsDeleteWallet({ + path: { address: wallet.address }, + })).data, onSuccess: () => { setOpen(false) onSuccess() diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/WalletRow.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/WalletRow.tsx index e9a853c5c5..fbd7734b61 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/WalletRow.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagement/WalletRow.tsx @@ -34,11 +34,11 @@ export function WalletRow({ }, [wallet.name, editing]) const mutation = useMutation({ - mutationFn: (name: string | null) => - WalletsService.updateWallet({ - address: wallet.address, - requestBody: { name }, - }), + mutationFn: async (name: string | null) => + (await WalletsService.walletsUpdateWallet({ + path: { address: wallet.address }, + body: { name }, + })).data, onSuccess: () => { setEditing(false) setRenameError(null) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagementCard.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagementCard.tsx index 00321fd859..5436652e41 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagementCard.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/WalletManagementCard.tsx @@ -18,7 +18,8 @@ export function WalletManagementCard() { const { user, logout } = useAuth() const { data: wallets = [], isLoading } = useQuery({ queryKey: ["wallets"], - queryFn: () => WalletsService.listWallets(), + queryFn: async () => + (await WalletsService.walletsListWallets()).data, }) const refresh = () => { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportCard.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportCard.tsx index 4ea7718d33..01856cc946 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportCard.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportCard.tsx @@ -47,7 +47,7 @@ export function SupportCard() { }) const createMutation = useMutation({ - mutationFn: () => + mutationFn: async () => createSupportTicket({ title: title.trim(), message: message.trim() }), onSuccess: () => { setOpen(false) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportChat.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportChat.tsx index d3f37b1011..09607d6269 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportChat.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Support/SupportChat.tsx @@ -35,7 +35,7 @@ export function SupportChat({ const { data: messages = [], isLoading } = useQuery({ queryKey: threadKey(nodeId), - queryFn: () => getThread(nodeId), + queryFn: async () => getThread(nodeId), refetchInterval: 8_000, }) @@ -43,7 +43,7 @@ export function SupportChat({ queryClient.invalidateQueries({ queryKey: threadKey(nodeId) }) const sendMutation = useMutation({ - mutationFn: (text: string) => sendMessage(nodeId, text), + mutationFn: async (text: string) => sendMessage(nodeId, text), onSuccess: () => { setDraft("") refresh() @@ -52,7 +52,7 @@ export function SupportChat({ }) const attachMutation = useMutation({ - mutationFn: (file: File) => + mutationFn: async (file: File) => file.arrayBuffer().then((buf) => sendAttachment(nodeId, { bytes: new Uint8Array(buf), @@ -66,7 +66,10 @@ export function SupportChat({ const debugMutation = useMutation({ mutationFn: async () => { - const state = await DebugService.getDebug({}) + const state = (await DebugService.debugGetDebug({})).data + if (!state) { + throw new Error("Debug state unavailable") + } const wallet = localStorage.getItem("auth_username") ?? "" const file = debugStateToFile(state, wallet) await sendAttachment(nodeId, file, "Shared a debug snapshot") @@ -79,7 +82,7 @@ export function SupportChat({ }) const closeMutation = useMutation({ - mutationFn: () => closeTicket(nodeId), + mutationFn: async () => closeTicket(nodeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: SUPPORT_TICKET_QUERY_KEY }) }, diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ExportResultsContent.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ExportResultsContent.tsx index a052c3a7a4..5b8188fcdc 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ExportResultsContent.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ExportResultsContent.tsx @@ -23,8 +23,7 @@ import { } from "lucide-react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import type { Task_Output as Task } from "@/client" -import { TasksService } from "@/client" +import { TasksService, type TaskOutput as Task } from "@/client" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -214,12 +213,14 @@ export default function ExportResultsContent({ setIsDecrypting(true) let failCount = 0 try { - const exportedResults = await TasksService.exportResults({ - requestBody: { + const exportResponse = await TasksService.tasksExportResults({ + body: { task_ids: stoppedTaskIds, user_rsa_public_key: userRsaPublicPem, }, + throwOnError: true, }) + const exportedResults = exportResponse.data const base = buildExportRows(tasks) const rows = await Promise.all( tasks.map(async (task, i) => { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportSteps/EncryptStep.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportSteps/EncryptStep.tsx index a27b68636b..6b10ee01a9 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportSteps/EncryptStep.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportSteps/EncryptStep.tsx @@ -7,7 +7,7 @@ import { } from "lucide-react" import { useEffect, useState } from "react" -import type { Task_Input as Task } from "@/client" +import type { TaskInput as Task } from "@/client" import { NodesService } from "@/client" import { Button } from "@/components/ui/button" import { LoadingButton } from "@/components/ui/loading-button" @@ -43,6 +43,11 @@ function getValidActions(actions: ActionRow[]): ActionRow[] { }) } +type NodeConfigEncryptionFields = { + tasks_encryption_enabled?: boolean + server_encryption_env_vars?: string[] +} + function buildContentString(action: ActionRow): string { const template = getTemplateById(action.templateId) const actions = template?.actionTypes.join(",") ?? "" @@ -64,14 +69,11 @@ export default function EncryptStep({ const { showErrorToast } = useCustomToast() useEffect(() => { - NodesService.getNodeConfig() - .then((data) => { - const d = data as { - tasks_encryption_enabled?: boolean - server_encryption_env_vars?: string[] - } - setEncryptionEnabled(d.tasks_encryption_enabled ?? false) - setEnvVars(d.server_encryption_env_vars ?? []) + void NodesService.nodesGetNodeConfig({ throwOnError: true }) + .then((response) => { + const nodeConfig = response.data as NodeConfigEncryptionFields + setEncryptionEnabled(nodeConfig.tasks_encryption_enabled ?? false) + setEnvVars(nodeConfig.server_encryption_env_vars ?? []) }) .catch(() => setEncryptionEnabled(false)) hasStoredClientKeys().then(setClientKeysStored) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportTask.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportTask.tsx index 3b7b5dec18..c982faecf6 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportTask.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/components/Tasks/ImportTask.tsx @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query" import { useState } from "react" -import { type Task_Input as Task, TasksService } from "@/client" +import { type TaskInput as Task, TasksService } from "@/client" import useCustomToast from "@/hooks/useCustomToast" import type { CSVRawResult } from "@/lib/csv" import ColumnMappingStep, { @@ -34,8 +34,8 @@ export default function ImportTask({ onSuccess }: ImportTaskProps) { const { showSuccessToast, showErrorToast } = useCustomToast() const createTaskMutation = useMutation({ - mutationFn: (data: Array) => - TasksService.createTasks({ requestBody: data }), + mutationFn: async (data: Array) => + (await TasksService.tasksCreateTasks({ body: data })).data, }) const handleCsvParsed = (result: CSVRawResult) => { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useAuth.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useAuth.ts index bacb07f1af..28861486c2 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useAuth.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useAuth.ts @@ -2,7 +2,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" import { useEffect } from "react" -import { type ApiError, type User, UsersService } from "@/client" +import { type User, UsersService } from "@/client" +import type { ApiError } from "@/lib/api-error" import { clearPassword, savePassword } from "@/lib/device-key" import { verifyLoginCredentials } from "@/lib/verify-login-credentials" import { setStoredIsSuperuser } from "@/lib/user-menu-display" @@ -34,7 +35,12 @@ const useAuth = () => { const { data: user, isPending } = useQuery({ queryKey: ["currentUser"], - queryFn: UsersService.readUserMe, + queryFn: async (): Promise => { + const response = await UsersService.usersReadUserMe({ + throwOnError: true, + }) + return response.data + }, enabled: isLoggedIn(), }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useLocalNetworkHostname.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useLocalNetworkHostname.ts index a52a144273..85a83f530e 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useLocalNetworkHostname.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useLocalNetworkHostname.ts @@ -9,7 +9,8 @@ function isUsableBrowserHostname(hostname: string): boolean { export function useLocalNetworkHostname() { const localNetworkQuery = useQuery({ queryKey: ["setup", "local-network-address"], - queryFn: () => SetupService.getLocalNetworkAddress(), + queryFn: async () => + (await SetupService.setupGetLocalNetworkAddress()).data, }) const browserHostname = window.location.hostname diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useVpnNetworkHostname.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useVpnNetworkHostname.ts index 628f694d3b..5b95fefdd7 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useVpnNetworkHostname.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/hooks/useVpnNetworkHostname.ts @@ -5,7 +5,8 @@ import { SetupService } from "@/client" export function useVpnNetworkHostname() { const vpnNetworkQuery = useQuery({ queryKey: ["setup", "vpn-network-address"], - queryFn: () => SetupService.getVpnNetworkAddress(), + queryFn: async () => + (await SetupService.setupGetVpnNetworkAddress()).data, }) const detectedIp = vpnNetworkQuery.data?.vpn_network_ip ?? null diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/auth-error-messages.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/auth-error-messages.test.ts index 3ca9827f15..de8a5cef60 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/auth-error-messages.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/auth-error-messages.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import { ApiError } from "@/client/core/ApiError" +import { createTestApiError } from "@/lib/api-error" import { API_AUTH_ERROR_CODES, CLIENT_AUTH_ERROR_CODES, @@ -59,22 +59,12 @@ describe("applyLoginAuthPresentation", () => { describe("resolveLoginAuthPresentation", () => { it("maps invalid passphrase API code with no tips on single-wallet", () => { - const error = new ApiError( - { method: "GET", url: "/login/test" }, - { - url: "/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { + const error = createTestApiError(401, { detail: { code: API_AUTH_ERROR_CODES.AUTH_INVALID_PASSPHRASE, message: "Passphrase verification failed", }, - }, - }, - "Unauthorized", - ) + }) const presentation = resolveLoginAuthPresentation(error, "pw", { multiWallet: false, }) @@ -84,19 +74,9 @@ describe("resolveLoginAuthPresentation", () => { }) it("adds whitespace tip when passphrase has edge spaces", () => { - const error = new ApiError( - { method: "GET", url: "/login/test" }, - { - url: "/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { + const error = createTestApiError(401, { detail: { code: API_AUTH_ERROR_CODES.AUTH_INVALID_PASSPHRASE }, - }, - }, - "Unauthorized", - ) + }) const presentation = resolveLoginAuthPresentation(error, " demodemo", { multiWallet: false, }) @@ -105,19 +85,9 @@ describe("resolveLoginAuthPresentation", () => { }) it("returns empty guidance for passphrase required code", () => { - const error = new ApiError( - { method: "GET", url: "/login/test" }, - { - url: "/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { + const error = createTestApiError(401, { detail: { code: API_AUTH_ERROR_CODES.AUTH_PASSPHRASE_REQUIRED }, - }, - }, - "Unauthorized", - ) + }) const presentation = resolveLoginAuthPresentation(error, "") expect(presentation.guidance).toHaveLength(0) }) @@ -129,17 +99,7 @@ describe("resolveLoginAuthPresentation", () => { }) it("maps unknown ApiError status to network presentation", () => { - const error = new ApiError( - { method: "GET", url: "/x" }, - { - url: "/x", - ok: false, - status: 500, - statusText: "Error", - body: {}, - }, - "Error", - ) + const error = createTestApiError(500, {}) const presentation = resolveLoginAuthPresentation(error, "pw") expect(presentation.title).toBe("Can't connect") expect(presentation.guidance).toHaveLength(1) @@ -148,70 +108,30 @@ describe("resolveLoginAuthPresentation", () => { describe("shouldSuppressLoginErrorToast", () => { it("suppresses 401 auth errors", () => { - const error = new ApiError( - { method: "GET", url: "/login/test" }, - { - url: "/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { + const error = createTestApiError(401, { detail: { code: API_AUTH_ERROR_CODES.AUTH_INVALID_PASSPHRASE }, - }, - }, - "Unauthorized", - ) + }) expect(shouldSuppressLoginErrorToast(error)).toBe(true) }) it("does not suppress generic 503", () => { - const error = new ApiError( - { method: "GET", url: "/x" }, - { - url: "/x", - ok: false, - status: 503, - statusText: "Unavailable", - body: { detail: { code: "other" } }, - }, - "Unavailable", - ) + const error = createTestApiError(503, { detail: { code: "other" } }) expect(shouldSuppressLoginErrorToast(error)).toBe(false) }) it("suppresses node not configured 503", () => { - const error = new ApiError( - { method: "GET", url: "/x" }, - { - url: "/x", - ok: false, - status: 503, - statusText: "Unavailable", - body: { + const error = createTestApiError(503, { detail: { code: API_AUTH_ERROR_CODES.AUTH_NODE_NOT_CONFIGURED }, - }, - }, - "Unavailable", - ) + }) expect(shouldSuppressLoginErrorToast(error)).toBe(true) }) }) describe("resolveLoginFormAuthError", () => { it("maps wallet not found API code", () => { - const error = new ApiError( - { method: "GET", url: "/login/test" }, - { - url: "/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { + const error = createTestApiError(401, { detail: { code: API_AUTH_ERROR_CODES.AUTH_WALLET_NOT_FOUND }, - }, - }, - "Unauthorized", - ) + }) const message = resolveLoginFormAuthError(error, "pw") expect(message).toContain("Wallet not found here") expect(message).toContain("same node address") @@ -220,19 +140,9 @@ describe("resolveLoginFormAuthError", () => { describe("shouldResetMultiWalletSelectionOnLoginError", () => { it("returns true for invalid passphrase", () => { - const error = new ApiError( - { method: "GET", url: "/login/test" }, - { - url: "/login/test", - ok: false, - status: 401, - statusText: "Unauthorized", - body: { + const error = createTestApiError(401, { detail: { code: API_AUTH_ERROR_CODES.AUTH_INVALID_PASSPHRASE }, - }, - }, - "Unauthorized", - ) + }) expect(shouldResetMultiWalletSelectionOnLoginError(error)).toBe(true) }) }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/configure-api-client.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/configure-api-client.test.ts new file mode 100644 index 0000000000..a74261e170 --- /dev/null +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/configure-api-client.test.ts @@ -0,0 +1,106 @@ +import type { InternalAxiosRequestConfig } from "axios" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { client } from "@/client/client.gen" + +vi.mock("@/lib/device-key", () => ({ + loadPassword: vi.fn(), +})) + +vi.mock("@/hooks/useAuth", () => ({ + clearAuth: vi.fn(() => Promise.resolve()), +})) + +vi.mock("@/lib/open-api-401-login-redirect", () => ({ + shouldRedirectToLoginOn401: vi.fn(() => true), +})) + +import { loadPassword } from "@/lib/device-key" +import { clearAuth } from "@/hooks/useAuth" +import { shouldRedirectToLoginOn401 } from "@/lib/open-api-401-login-redirect" +import { + configureApiClient, + resetApiClientConfigurationForTests, +} from "@/lib/configure-api-client" + +function getRequestInterceptor() { + const manager = client.instance.interceptors.request as unknown as { + handlers: Array<{ fulfilled: (config: InternalAxiosRequestConfig) => unknown }> + } + const handler = manager.handlers[manager.handlers.length - 1] + if (!handler?.fulfilled) { + throw new Error("request interceptor not registered") + } + return handler.fulfilled +} + +function getResponseInterceptor() { + const manager = client.instance.interceptors.response as unknown as { + handlers: Array<{ fulfilled: (response: { status: number }) => unknown }> + } + const handler = manager.handlers[manager.handlers.length - 1] + if (!handler?.fulfilled) { + throw new Error("response interceptor not registered") + } + return handler.fulfilled +} + +const localStorageStore: Record = {} + +describe("configureApiClient", () => { + beforeEach(() => { + resetApiClientConfigurationForTests() + Object.keys(localStorageStore).forEach((key) => { + delete localStorageStore[key] + }) + vi.stubGlobal("localStorage", { + getItem: (key: string) => localStorageStore[key] ?? null, + setItem: (key: string, value: string) => { + localStorageStore[key] = value + }, + removeItem: (key: string) => { + delete localStorageStore[key] + }, + clear: () => { + Object.keys(localStorageStore).forEach((key) => { + delete localStorageStore[key] + }) + }, + }) + vi.mocked(loadPassword).mockReset() + vi.mocked(clearAuth).mockClear() + vi.mocked(shouldRedirectToLoginOn401).mockReturnValue(true) + vi.stubGlobal("sessionStorage", { + getItem: () => null, + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + }) + vi.stubGlobal("window", { + location: { pathname: "/app/octobots", href: "" }, + }) + }) + + it("request interceptor adds Basic Authorization when credentials exist", async () => { + localStorage.setItem("auth_username", "0xwallet") + vi.mocked(loadPassword).mockResolvedValue("passphrase") + configureApiClient() + + const axios = await import("axios") + const runRequest = getRequestInterceptor() + const config = { + headers: new axios.AxiosHeaders(), + } as InternalAxiosRequestConfig + + const updated = (await runRequest(config)) as InternalAxiosRequestConfig + expect(String(updated.headers.get("Authorization"))).toMatch(/^Basic /) + }) + + it("response interceptor redirects on 401 when policy allows", async () => { + configureApiClient() + const runResponse = getResponseInterceptor() + await runResponse({ status: 401 }) + expect(clearAuth).toHaveBeenCalled() + expect(window.location.href).toBe("/app/login") + }) +}) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/export-results-fetch.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/export-results-fetch.test.ts index f15d84968e..f5356456c1 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/export-results-fetch.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/export-results-fetch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { getStoppedTaskIdsForExport, shouldProcessExportEntry, diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/feedback-share-api.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/feedback-share-api.test.ts index 08b584617a..fb00ee37ab 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/feedback-share-api.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/feedback-share-api.test.ts @@ -2,15 +2,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" vi.mock("@/client", () => ({ FeedbackService: { - getFeedbackPreview: vi.fn(), - exportFeedback: vi.fn(), + feedbackGetFeedbackPreview: vi.fn(), + feedbackExportFeedback: vi.fn(), }, })) import { FeedbackService } from "@/client" import { submitFeedbackDownload } from "@/lib/feedback-share" -const mockedExportFeedback = vi.mocked(FeedbackService.exportFeedback) +const mockedExportFeedback = vi.mocked(FeedbackService.feedbackExportFeedback) const assignMock = vi.fn() function stubWindowPathname(pathname: string) { @@ -22,6 +22,10 @@ function stubWindowPathname(pathname: string) { }) } +function mockExportResolved(envelope: unknown) { + mockedExportFeedback.mockResolvedValue({ data: envelope } as never) +} + describe("submitFeedbackDownload", () => { const createObjectUrlMock = vi.fn(() => "blob:feedback") const revokeObjectUrlMock = vi.fn() @@ -67,7 +71,7 @@ describe("submitFeedbackDownload", () => { event_count: 2, uploaded: false, } - mockedExportFeedback.mockResolvedValue(exportEnvelope as never) + mockExportResolved(exportEnvelope) await submitFeedbackDownload({ note: "App froze on settings", @@ -80,13 +84,14 @@ describe("submitFeedbackDownload", () => { }) expect(mockedExportFeedback).toHaveBeenCalledWith({ - requestBody: { + body: { note: "App froze on settings\n\n[contact] method=email value=user@example.com", issue_url: null, ui_error_name: "boot_failed", ui_error_route: "/app/settings", }, + throwOnError: true, }) expect(createObjectUrlMock).toHaveBeenCalledTimes(1) expect(clickMock).toHaveBeenCalledTimes(1) @@ -98,7 +103,7 @@ describe("submitFeedbackDownload", () => { it("exports ui_error_route from current page for navbar feedback", async () => { stubWindowPathname("/app/settings") - mockedExportFeedback.mockResolvedValue({ install_id: "x" } as never) + mockExportResolved({ install_id: "x" }) await submitFeedbackDownload({ note: "Navbar feedback", @@ -106,29 +111,31 @@ describe("submitFeedbackDownload", () => { }) expect(mockedExportFeedback).toHaveBeenCalledWith({ - requestBody: { + body: { note: "[ui_context] source=navbar\n\nNavbar feedback", issue_url: null, ui_error_name: null, ui_error_route: "/app/settings", }, + throwOnError: true, }) }) it("exports route error context on envelope request fields", async () => { - mockedExportFeedback.mockResolvedValue({ install_id: "x" } as never) + mockExportResolved({ install_id: "x" }) await submitFeedbackDownload({ context: { source: "route_error", routePath: "/app/x" }, }) expect(mockedExportFeedback).toHaveBeenCalledWith({ - requestBody: { + body: { note: null, issue_url: null, ui_error_name: "route_error", ui_error_route: "/app/x", }, + throwOnError: true, }) }) @@ -138,31 +145,11 @@ describe("submitFeedbackDownload", () => { const envelope = await submitFeedbackDownload({ note: "Recovery note", - context: { - source: "recovery", - failureKind: "auth_broken", - }, + context: { source: "recovery", failureKind: "insecure_context" }, }) - expect(envelope.install_id).toBe("recovery-client-fallback") - expect(envelope.note).toBe("Recovery note") - expect(envelope.ui_error_name).toBe("auth_broken") - expect(envelope.ui_error_route).toBe("/app/insecure") + expect(envelope.uploaded).toBe(false) expect(createObjectUrlMock).toHaveBeenCalledTimes(1) expect(clickMock).toHaveBeenCalledTimes(1) - expect(assignMock).toHaveBeenCalledTimes(1) - }) - - it("rethrows export errors for non-recovery context", async () => { - mockedExportFeedback.mockRejectedValue(new Error("network error")) - - await expect( - submitFeedbackDownload({ - note: "Navbar feedback", - context: { source: "navbar" }, - }), - ).rejects.toThrow("network error") - expect(createObjectUrlMock).not.toHaveBeenCalled() - expect(assignMock).not.toHaveBeenCalled() }) }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/journal-client-event.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/journal-client-event.test.ts index ede223ab58..d45fc4076e 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/journal-client-event.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/journal-client-event.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" -import { OpenAPI } from "@/client" +vi.mock("@/lib/configure-api-client", () => ({ + getApiBaseUrl: () => "http://localhost:8000", +})) import { buildJournalClientEventUrl, getOrCreateClientInstanceId, @@ -57,7 +59,6 @@ describe("buildJournalClientEventUrl", () => { describe("reportUiJournalEvent", () => { beforeEach(() => { sessionStorageMock.clear() - OpenAPI.BASE = "http://localhost:8000" fetchMock.mockReset() fetchMock.mockResolvedValue({ ok: true }) vi.stubGlobal("fetch", fetchMock) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/api-error.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/api-error.ts new file mode 100644 index 0000000000..0633ac706c --- /dev/null +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/api-error.ts @@ -0,0 +1,63 @@ +import axios, { type AxiosError } from "axios" + +export type ApiError = AxiosError + +export function isApiError(error: unknown): error is ApiError { + return axios.isAxiosError(error) +} + +export function getApiErrorStatus(error: ApiError): number | undefined { + return error.response?.status +} + +/** Build an Axios-shaped error for unit tests (replaces legacy `ApiError` class). */ +export function createTestApiError( + status: number, + body: unknown, + message = "Request failed", +): ApiError { + return { + isAxiosError: true, + message, + name: "AxiosError", + response: { + status, + data: body, + statusText: "", + headers: {}, + config: {} as ApiError["config"], + }, + config: {} as ApiError["config"], + toJSON: () => ({}), + } as ApiError +} + +export function formatApiErrorDetailMessage( + error: unknown, + fallback: string, +): string { + if (isApiError(error)) { + const body = getApiErrorResponseBody(error) + const detail = (body as { detail?: unknown } | undefined)?.detail + if (typeof detail === "string" && detail.trim().length > 0) { + return detail + } + if (Array.isArray(detail) && detail.length > 0) { + const first = detail[0] as { msg?: string } + if (typeof first?.msg === "string" && first.msg.trim().length > 0) { + return first.msg + } + } + if (error.message.trim().length > 0) { + return error.message + } + } + if (error instanceof Error && error.message.trim().length > 0) { + return error.message + } + return fallback +} + +export function getApiErrorResponseBody(error: ApiError): unknown { + return error.response?.data +} diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/auth-error-messages.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/auth-error-messages.ts index fec918c4e6..300aa7a7b2 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/auth-error-messages.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/auth-error-messages.ts @@ -3,7 +3,8 @@ * whitespace are not removed. */ -import { ApiError } from "@/client" +import type { ApiError } from "@/lib/api-error" +import { isApiError } from "@/lib/api-error" import { API_AUTH_ERROR_CODES, type ApiAuthErrorCode, @@ -172,7 +173,7 @@ export function resolveLoginAuthPresentation( ) } - if (!(error instanceof ApiError)) { + if (!isApiError(error)) { return applyLoginAuthPresentation( CLIENT_AUTH_ERROR_CODES.DEVICE_STORAGE_BLOCKED, context, @@ -180,7 +181,8 @@ export function resolveLoginAuthPresentation( ) } - if (error.status === 503) { + const status = error.response?.status + if (status === 503) { const code = parseAuthErrorCodeFromApiError(error) if (code === API_AUTH_ERROR_CODES.AUTH_NODE_NOT_CONFIGURED) { return applyLoginAuthPresentation(code, context, false) @@ -192,7 +194,7 @@ export function resolveLoginAuthPresentation( ) } - if (error.status === 401) { + if (status === 401) { const code = parseAuthErrorCodeFromApiError(error) ?? API_AUTH_ERROR_CODES.AUTH_INVALID_PASSPHRASE @@ -211,13 +213,14 @@ export function shouldSuppressLoginErrorToast(error: unknown): boolean { if (error instanceof Error && error.message === "Authentication failed") { return true } - if (!(error instanceof ApiError)) { + if (!isApiError(error)) { return true } - if (error.status === 401) { + const status = error.response?.status + if (status === 401) { return true } - if (error.status === 503) { + if (status === 503) { const code = parseAuthErrorCodeFromApiError(error) return code === API_AUTH_ERROR_CODES.AUTH_NODE_NOT_CONFIGURED } @@ -237,7 +240,7 @@ export function resolveLoginFormAuthError( export function shouldResetMultiWalletSelectionOnLoginError( error: unknown, ): boolean { - if (!(error instanceof ApiError) || error.status !== 401) { + if (!isApiError(error) || error.response?.status !== 401) { return false } const code = parseAuthErrorCodeFromApiError(error) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/configure-api-client.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/configure-api-client.ts new file mode 100644 index 0000000000..81c49ce882 --- /dev/null +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/configure-api-client.ts @@ -0,0 +1,59 @@ +import { client } from "@/client/client.gen" +import { buildBasicAuthorizationHeader } from "@/lib/basic-auth" +import { loadPassword } from "@/lib/device-key" +import { markLoginSessionCleared } from "@/lib/login-session-hint" +import { shouldRedirectToLoginOn401 } from "@/lib/open-api-401-login-redirect" +import { clearAuth } from "@/hooks/useAuth" +import { resolveApiBaseUrl } from "@/lib/hey-api-client-config" + +let isApiClientConfigured = false +let isRedirectingOnAuthFailure = false + +export function getApiBaseUrl(): string { + return resolveApiBaseUrl() +} + +export function resetApiClientConfigurationForTests(): void { + isApiClientConfigured = false + isRedirectingOnAuthFailure = false +} + +export function configureApiClient(): void { + if (isApiClientConfigured) { + return + } + isApiClientConfigured = true + + client.setConfig({ + baseURL: getApiBaseUrl(), + throwOnError: true, + }) + + client.instance.interceptors.request.use(async (config) => { + const username = localStorage.getItem("auth_username") ?? "" + const password = (await loadPassword()) ?? "" + if (username && password) { + config.headers.set( + "Authorization", + buildBasicAuthorizationHeader(username, password), + ) + } + return config + }) + + client.instance.interceptors.response.use((response) => { + if ( + shouldRedirectToLoginOn401(response, { + pathname: window.location.pathname, + isRedirectingOnAuthFailure, + }) + ) { + isRedirectingOnAuthFailure = true + markLoginSessionCleared() + void clearAuth().finally(() => { + window.location.href = "/app/login" + }) + } + return response + }) +} diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/__tests__/accounts-historical-values.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/__tests__/accounts-historical-values.test.ts new file mode 100644 index 0000000000..3170e7dccd --- /dev/null +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/__tests__/accounts-historical-values.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +vi.mock("@/client", () => ({ + AccountsService: { + accountsGetAccountHistoricalValues: vi.fn(), + accountsGetAggregatedAccountHistoricalValues: vi.fn(), + }, +})) + +import { AccountsService } from "@/client" +import { fetchAccountHistoricalValues } from "@/lib/debug/account-historical-values-api" +import { fetchAggregatedAccountHistoricalValues } from "@/lib/debug/aggregated-account-historical-values-api" + +const mockedAccountHistorical = vi.mocked( + AccountsService.accountsGetAccountHistoricalValues, +) +const mockedAggregatedHistorical = vi.mocked( + AccountsService.accountsGetAggregatedAccountHistoricalValues, +) + +describe("fetchAccountHistoricalValues", () => { + beforeEach(() => { + mockedAccountHistorical.mockReset() + mockedAccountHistorical.mockResolvedValue({ + data: { series: [] }, + } as never) + }) + + it("passes account_id path and wallet_address query", async () => { + await fetchAccountHistoricalValues("acct-1", "0xabc") + expect(mockedAccountHistorical).toHaveBeenCalledWith({ + path: { account_id: "acct-1" }, + query: { wallet_address: "0xabc" }, + throwOnError: true, + }) + }) + + it("uses null wallet_address when omitted", async () => { + await fetchAccountHistoricalValues("acct-2") + expect(mockedAccountHistorical).toHaveBeenCalledWith({ + path: { account_id: "acct-2" }, + query: { wallet_address: null }, + throwOnError: true, + }) + }) +}) + +describe("fetchAggregatedAccountHistoricalValues", () => { + beforeEach(() => { + mockedAggregatedHistorical.mockReset() + mockedAggregatedHistorical.mockResolvedValue({ + data: { series: [] }, + } as never) + }) + + it("passes is_simulated and wallet_address query", async () => { + await fetchAggregatedAccountHistoricalValues(true, "0xdef") + expect(mockedAggregatedHistorical).toHaveBeenCalledWith({ + query: { is_simulated: true, wallet_address: "0xdef" }, + throwOnError: true, + }) + }) +}) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/account-historical-values-api.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/account-historical-values-api.ts index 03a64372e4..afbf0aaff2 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/account-historical-values-api.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/account-historical-values-api.ts @@ -1,26 +1,14 @@ -import { OpenAPI } from "@/client/core/OpenAPI" -import { request } from "@/client/core/request" - +import { AccountsService } from "@/client" import type { PortfolioHistoricalValuesState } from "@/lib/debug/portfolio-historical-values-types" -/** - * Interim API helper until OpenAPI regen adds AccountsService.getAccountHistoricalValues. - */ -export function fetchAccountHistoricalValues( +export async function fetchAccountHistoricalValues( accountId: string, walletAddress?: string, -) { - return request(OpenAPI, { - method: "GET", - url: "/api/v1/accounts/{account_id}/historical-values", - path: { - account_id: accountId, - }, - query: { - wallet_address: walletAddress, - }, - errors: { - 422: "Validation Error", - }, +): Promise { + const response = await AccountsService.accountsGetAccountHistoricalValues({ + path: { account_id: accountId }, + query: { wallet_address: walletAddress ?? null }, + throwOnError: true, }) + return response.data as PortfolioHistoricalValuesState } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/aggregated-account-historical-values-api.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/aggregated-account-historical-values-api.ts index b7888c635f..2789b16654 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/aggregated-account-historical-values-api.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/aggregated-account-historical-values-api.ts @@ -1,24 +1,17 @@ -import { OpenAPI } from "@/client/core/OpenAPI" -import { request } from "@/client/core/request" - +import { AccountsService } from "@/client" import type { PortfolioHistoricalValuesState } from "@/lib/debug/portfolio-historical-values-types" -/** - * Interim API helper until OpenAPI regen adds aggregated historical values. - */ -export function fetchAggregatedAccountHistoricalValues( +export async function fetchAggregatedAccountHistoricalValues( isSimulated: boolean, walletAddress?: string, -) { - return request(OpenAPI, { - method: "GET", - url: "/api/v1/accounts/aggregated/historical-values", - query: { - is_simulated: isSimulated, - wallet_address: walletAddress, - }, - errors: { - 422: "Validation Error", - }, - }) +): Promise { + const response = + await AccountsService.accountsGetAggregatedAccountHistoricalValues({ + query: { + is_simulated: isSimulated, + wallet_address: walletAddress ?? null, + }, + throwOnError: true, + }) + return response.data as PortfolioHistoricalValuesState } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/queries.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/queries.ts index 23de22b30e..9e8035d1a3 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/queries.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/debug/queries.ts @@ -1,4 +1,4 @@ -import { DebugService } from "@/client" +import { type DebugState, DebugService } from "@/client" import { fetchAccountHistoricalValues } from "@/lib/debug/account-historical-values-api" import { fetchAggregatedAccountHistoricalValues } from "@/lib/debug/aggregated-account-historical-values-api" @@ -7,8 +7,10 @@ export function getDebugQueryOptions(walletAddress?: string | null) { walletAddress && walletAddress.length > 0 ? walletAddress : undefined return { queryKey: ["debug", resolved ?? "current"] as const, - queryFn: () => - DebugService.getDebug(resolved ? { walletAddress: resolved } : {}), + queryFn: async () => + (await DebugService.debugGetDebug( + resolved ? { query: { wallet_address: resolved } } : {}, + )).data, } } @@ -25,7 +27,7 @@ export function getAccountHistoricalValuesQueryOptions( accountId ?? "none", resolvedWallet ?? "current", ] as const, - queryFn: () => { + queryFn: async () => { if (!accountId) { throw new Error("Account id is required") } @@ -48,7 +50,7 @@ export function getAggregatedAccountHistoricalValuesQueryOptions( isSimulated ? "simulated" : "real", resolvedWallet ?? "current", ] as const, - queryFn: () => + queryFn: async () => fetchAggregatedAccountHistoricalValues(isSimulated, resolvedWallet), enabled, } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/export-results-fetch.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/export-results-fetch.ts index f90b3a8c1c..eb6118fc79 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/export-results-fetch.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/export-results-fetch.ts @@ -1,4 +1,4 @@ -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { getActiveExecution, getStatusGroup } from "@/utils/executions" export function getStoppedTaskIdsForExport(tasks: Task[]): string[] { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/feedback-share.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/feedback-share.ts index b57633725b..adee77e02b 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/feedback-share.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/feedback-share.ts @@ -39,7 +39,10 @@ export const FEEDBACK_JOURNAL_JSON_FILENAME = "node_journal.json" const FEEDBACK_JOURNAL_ZIP_MIME = "application/zip" export async function fetchFeedbackPreview(): Promise { - return FeedbackService.getFeedbackPreview() + const response = await FeedbackService.feedbackGetFeedbackPreview({ + throwOnError: true, + }) + return response.data } export function getPreviewEventCount(preview: FeedbackPreviewResponse): number { @@ -419,14 +422,16 @@ export async function submitFeedbackDownload({ }) let envelope: FeedbackUploadEnvelope try { - envelope = await FeedbackService.exportFeedback({ - requestBody: { + const response = await FeedbackService.feedbackExportFeedback({ + body: { note: composedNote || null, issue_url: null, ui_error_name: uiErrorName, ui_error_route: uiErrorRoute, }, + throwOnError: true, }) + envelope = response.data } catch (exportError) { if (context.source !== "recovery") { throw exportError diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/hey-api-client-config.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/hey-api-client-config.ts new file mode 100644 index 0000000000..e63236cc9b --- /dev/null +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/hey-api-client-config.ts @@ -0,0 +1,14 @@ +import type { CreateClientConfig } from "../client/client.gen" + +export function resolveApiBaseUrl(): string { + return ( + import.meta.env.NODE_API_URL || + (import.meta.env.DEV ? "http://localhost:8000" : "") + ) +} + +export const createClientConfig: CreateClientConfig = (config) => ({ + ...config, + baseURL: resolveApiBaseUrl(), + throwOnError: true, +}) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/journal-client-event.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/journal-client-event.ts index c80b8ee207..62061ad465 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/journal-client-event.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/journal-client-event.ts @@ -1,4 +1,4 @@ -import { OpenAPI } from "@/client" +import { getApiBaseUrl } from "@/lib/configure-api-client" export type UiJournalEvent = | "ui_boot_failed" @@ -33,7 +33,7 @@ export function getOrCreateClientInstanceId(): string { } export function resolveJournalApiBase(): string { - return OpenAPI.BASE + return getApiBaseUrl() } export function buildJournalClientEventUrl(apiBase: string): string { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/create-generic-process-bot.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/create-generic-process-bot.test.ts index bbe9caefe4..064b562d34 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/create-generic-process-bot.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/create-generic-process-bot.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import { ApiError } from "@/client" +import { createTestApiError } from "@/lib/api-error" import { buildCreateGenericProcessBotRequestBody, formatCreateGenericProcessBotError, @@ -37,34 +37,16 @@ describe("buildCreateGenericProcessBotRequestBody", () => { describe("formatCreateGenericProcessBotError", () => { it("uses string detail from ApiError", () => { - const error = new ApiError( - { method: "POST", url: "/api/v1/octobots/generic-process" }, - { - url: "/api/v1/octobots/generic-process", - ok: false, - status: 400, - statusText: "Bad Request", - body: { detail: "name must not be empty" }, - }, - "Bad Request", - ) + const error = createTestApiError(400, { detail: "name must not be empty" }) expect(formatCreateGenericProcessBotError(error)).toBe( "name must not be empty", ) }) it("uses validation array detail from ApiError", () => { - const error = new ApiError( - { method: "POST", url: "/api/v1/octobots/generic-process" }, - { - url: "/api/v1/octobots/generic-process", - ok: false, - status: 422, - statusText: "Unprocessable Entity", - body: { detail: [{ msg: "Field required" }] }, - }, - "Unprocessable Entity", - ) + const error = createTestApiError(422, { + detail: [{ msg: "Field required" }], + }) expect(formatCreateGenericProcessBotError(error)).toBe("Field required") }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/stop-automation.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/stop-automation.test.ts index bfdbb9f293..330fe1c0bd 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/stop-automation.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/stop-automation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import { ApiError } from "@/client" +import { createTestApiError } from "@/lib/api-error" import { resolveOneOfInstance } from "@/lib/debug/protocol-oneof" import { buildStopAutomationUserAction, @@ -95,32 +95,14 @@ describe("buildStopAutomationUserAction", () => { describe("formatStopAutomationError", () => { it("uses string detail from ApiError", () => { - const error = new ApiError( - { method: "POST", url: "/api/v1/debug/" }, - { - url: "/api/v1/debug/", - ok: false, - status: 400, - statusText: "Bad Request", - body: { detail: "automation not found" }, - }, - "Bad Request", - ) + const error = createTestApiError(400, { detail: "automation not found" }) expect(formatStopAutomationError(error)).toBe("automation not found") }) it("uses validation array detail from ApiError", () => { - const error = new ApiError( - { method: "POST", url: "/api/v1/debug/" }, - { - url: "/api/v1/debug/", - ok: false, - status: 422, - statusText: "Unprocessable Entity", - body: { detail: [{ msg: "Invalid user action" }] }, - }, - "Unprocessable Entity", - ) + const error = createTestApiError(422, { + detail: [{ msg: "Invalid user action" }], + }) expect(formatStopAutomationError(error)).toBe("Invalid user action") }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/task-matches-search-query.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/task-matches-search-query.test.ts index b03fb0ba77..cc3e270a50 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/task-matches-search-query.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/__tests__/task-matches-search-query.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { taskMatchesSearchQuery } from "@/lib/octobots/task-matches-search-query" function makeTask(overrides: Partial = {}): Task { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/create-generic-process-bot.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/create-generic-process-bot.ts index 93630adc7a..216a556ce5 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/create-generic-process-bot.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/create-generic-process-bot.ts @@ -1,4 +1,4 @@ -import type { ApiError } from "@/client" +import { formatApiErrorDetailMessage } from "@/lib/api-error" export type CreateGenericProcessBotNameValidation = | { valid: true; trimmedName: string } @@ -19,24 +19,8 @@ export function buildCreateGenericProcessBotRequestBody(trimmedName: string) { } export function formatCreateGenericProcessBotError(error: unknown): string { - if (error instanceof Error && error.name === "ApiError") { - const apiError = error as ApiError - const detail = (apiError.body as { detail?: unknown } | undefined)?.detail - if (typeof detail === "string" && detail.trim().length > 0) { - return detail - } - if (Array.isArray(detail) && detail.length > 0) { - const first = detail[0] as { msg?: string } - if (typeof first?.msg === "string" && first.msg.trim().length > 0) { - return first.msg - } - } - if (apiError.message.trim().length > 0) { - return apiError.message - } - } - if (error instanceof Error && error.message.trim().length > 0) { - return error.message - } - return "Couldn't create your OctoBot. Try again." + return formatApiErrorDetailMessage( + error, + "Couldn't create your OctoBot. Try again.", + ) } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/stop-automation.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/stop-automation.ts index a7041f310c..57d7a2b4fb 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/stop-automation.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/stop-automation.ts @@ -1,4 +1,5 @@ -import type { ApiError, Task_Output as Task, UserAction } from "@/client" +import type { TaskOutput as Task, UserAction } from "@/client" +import { formatApiErrorDetailMessage } from "@/lib/api-error" import { buildAutomationStopUserActionJson } from "@/lib/debug/user-action-templates" import { resolveOneOfInstance } from "@/lib/debug/protocol-oneof" import { getActiveExecution, getStatusGroup } from "@/utils/executions" @@ -18,29 +19,10 @@ export function buildStopAutomationUserAction( } export function formatStopAutomationError(error: unknown): string { - if (error instanceof Error && error.name === "ApiError") { - const apiError = error as ApiError - const detail = (apiError.body as { detail?: unknown } | undefined)?.detail - if (typeof detail === "string" && detail.trim().length > 0) { - return detail - } - if (Array.isArray(detail) && detail.length > 0) { - const firstDetail = detail[0] as { msg?: string } - if ( - typeof firstDetail?.msg === "string" && - firstDetail.msg.trim().length > 0 - ) { - return firstDetail.msg - } - } - if (apiError.message.trim().length > 0) { - return apiError.message - } - } - if (error instanceof Error && error.message.trim().length > 0) { - return error.message - } - return "Couldn't stop this OctoBot. Try again." + return formatApiErrorDetailMessage( + error, + "Couldn't stop this OctoBot. Try again.", + ) } export function getStopAutomationConfigurationActionType( diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/task-matches-search-query.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/task-matches-search-query.ts index 758f06a27b..00b72115e0 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/task-matches-search-query.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/octobots/task-matches-search-query.ts @@ -1,4 +1,4 @@ -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { getActiveExecution } from "@/utils/executions" export function taskMatchesSearchQuery(task: Task, query: string): boolean { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/pairing.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/pairing.ts index 2e3e5ca7ea..5c144d9b3d 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/pairing.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/pairing.ts @@ -45,17 +45,17 @@ export async function resolvePairingNodeUrl(): Promise { // P1: Tailscale IP, P2: LAN IP, P3: browser origin fallback. const [vpnResult, localResult] = await Promise.allSettled([ - SetupService.getVpnNetworkAddress(), - SetupService.getLocalNetworkAddress(), + (await SetupService.setupGetVpnNetworkAddress()).data, + (await SetupService.setupGetLocalNetworkAddress()).data, ]) const vpnIp = vpnResult.status === "fulfilled" - ? (vpnResult.value.vpn_network_ip ?? null) + ? (vpnResult.value?.vpn_network_ip ?? null) : null const localIp = localResult.status === "fulfilled" - ? (localResult.value.local_network_ip ?? null) + ? (localResult.value?.local_network_ip ?? null) : null return resolvePairingNodeHostname(vpnIp, localIp, window.location.origin) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/parse-auth-api-error.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/parse-auth-api-error.ts index f887168e2a..d4db9e7e03 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/parse-auth-api-error.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/parse-auth-api-error.ts @@ -1,6 +1,6 @@ -import { ApiError } from "@/client" import type { ApiAuthErrorCode } from "@/lib/auth-error-codes" import { API_AUTH_ERROR_CODES } from "@/lib/auth-error-codes" +import { getApiErrorResponseBody, isApiError } from "@/lib/api-error" const API_AUTH_CODE_SET = new Set(Object.values(API_AUTH_ERROR_CODES)) @@ -29,8 +29,8 @@ export function parseAuthErrorCodeFromBody(body: unknown): ApiAuthErrorCode | nu export function parseAuthErrorCodeFromApiError( error: unknown, ): ApiAuthErrorCode | null { - if (!(error instanceof ApiError)) { + if (!isApiError(error)) { return null } - return parseAuthErrorCodeFromBody(error.body) + return parseAuthErrorCodeFromBody(getApiErrorResponseBody(error)) } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/server-keys.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/server-keys.ts index 8a6f260753..e8bb503514 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/server-keys.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/server-keys.ts @@ -9,7 +9,7 @@ let cache: ServerPublicKeys | null = null export async function fetchServerPublicKeys(): Promise { if (cache) return cache - const raw = await TasksService.getServerPublicKeys() + const raw = (await TasksService.tasksGetServerPublicKeys()).data const data = raw as { server_rsa_public_pem: string server_ecdsa_public_pem: string diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/task-queries.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/task-queries.ts index db48e0b735..d5f0ec27cb 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/task-queries.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/task-queries.ts @@ -1,4 +1,4 @@ -import { TasksService } from "@/client" +import { type TaskOutput, TasksService } from "@/client" // Must match octobot_node.constants.TASKS_LIST_MAX_PAGE_LIMIT (tasks beyond cap are not shown). const TASK_LIST_FETCH_LIMIT = 500 @@ -6,8 +6,13 @@ const TASKS_LIST_REFETCH_INTERVAL_MS = 5_000 export function getTasksQueryOptions() { return { - queryFn: () => - TasksService.getTasks({ page: 1, limit: TASK_LIST_FETCH_LIMIT }), + queryFn: async (): Promise => { + const response = await TasksService.tasksGetTasks({ + query: { page: 1, limit: TASK_LIST_FETCH_LIMIT }, + throwOnError: true, + }) + return response.data + }, queryKey: ["tasks"], refetchInterval: TASKS_LIST_REFETCH_INTERVAL_MS, } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/verify-login-credentials.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/verify-login-credentials.ts index c125701939..4004878bd9 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/verify-login-credentials.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/lib/verify-login-credentials.ts @@ -1,6 +1,5 @@ -import { ApiError } from "@/client" -import { OpenAPI } from "@/client/core/OpenAPI" import type { User } from "@/client" +import { LoginService } from "@/client" import { buildBasicAuthorizationHeader } from "@/lib/basic-auth" /** Verify wallet + passphrase without writing `auth_username` (avoids session probe races). */ @@ -8,29 +7,11 @@ export async function verifyLoginCredentials( username: string, password: string, ): Promise { - const base = (OpenAPI.BASE ?? "").replace(/\/$/, "") - const url = `${base}/api/v1/login/test` - const response = await fetch(url, { - headers: { Authorization: buildBasicAuthorizationHeader(username, password) }, + const response = await LoginService.loginTestAuth({ + headers: { + Authorization: buildBasicAuthorizationHeader(username, password), + }, + throwOnError: true, }) - let body: unknown = null - try { - body = await response.json() - } catch { - body = null - } - if (!response.ok) { - throw new ApiError( - { method: "GET", url }, - { - url, - ok: false, - status: response.status, - statusText: response.statusText, - body, - }, - response.statusText, - ) - } - return body as User + return response.data } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/__root.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/__root.tsx index a727522785..089efed519 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/__root.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/__root.tsx @@ -14,8 +14,10 @@ export const Route = createRootRoute({ beforeLoad: async ({ location }) => { let configured = true try { - const status = await SetupService.getSetupStatus() - configured = status.configured + const response = await SetupService.setupGetSetupStatus({ + throwOnError: true, + }) + configured = response.data.configured ?? false } catch { // network error — do not block navigation } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/_layout/octobots/index.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/_layout/octobots/index.tsx index 7ffe31259e..61a30302fd 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/_layout/octobots/index.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/_layout/octobots/index.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router" import { Loader2 } from "lucide-react" import { Suspense, useCallback, useMemo, useState } from "react" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { BotGrid } from "@/components/OctoBots/BotGrid" import { BotsFilterBar } from "@/components/OctoBots/BotsFilterBar" import { SelectionToolbar } from "@/components/OctoBots/SelectionToolbar" diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/login.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/login.tsx index fedd2a0b56..c203d4349f 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/login.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/login.tsx @@ -71,7 +71,8 @@ function Login() { isError: walletsError, } = useQuery({ queryKey: ["wallets"], - queryFn: () => WalletsService.listWallets(), + queryFn: async () => + (await WalletsService.walletsListWallets()).data, staleTime: 0, }) diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup.tsx index e225af98bf..aba23d08b3 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup.tsx @@ -7,7 +7,10 @@ export const Route = createFileRoute("/setup")({ beforeLoad: async ({ location }) => { let configured = false try { - configured = (await SetupService.getSetupStatus()).configured + const response = await SetupService.setupGetSetupStatus({ + throwOnError: true, + }) + configured = response.data.configured ?? false } catch { // network error — stay on setup } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup/index.tsx b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup/index.tsx index 446577b429..bf16d119bb 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup/index.tsx +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/routes/setup/index.tsx @@ -5,7 +5,8 @@ import { useState } from "react" import { useForm } from "react-hook-form" import { z } from "zod" -import { type ApiError, type SetupResult, SetupService } from "@/client" +import { type SetupResult, SetupService } from "@/client" +import { getApiErrorStatus, type ApiError } from "@/lib/api-error" import { AuthLayout } from "@/components/Common/AuthLayout" import { SetupStepHeader } from "@/components/Setup/SetupStepHeader" import { @@ -97,15 +98,18 @@ function SetupWallet() { ApiError, { passphrase: string; privateKey?: string; name?: string } >({ - mutationFn: ({ passphrase, privateKey, name }) => - SetupService.initSetup({ - requestBody: { + mutationFn: async ({ passphrase, privateKey, name }): Promise => { + const response = await SetupService.setupInitSetup({ + body: { passphrase, node_type: "standalone", private_key: privateKey || undefined, name: name?.trim() || undefined, }, - }), + throwOnError: true, + }) + return response.data + }, onSuccess: async (result, { passphrase, name }) => { // Persist the password before marking the session as logged in — if this // throws (e.g. IndexedDB blocked), auth_username must not be set, so @@ -127,7 +131,7 @@ function SetupWallet() { }, onError: async (error) => { const apiError = error as ApiError - if (apiError.status === 409) { + if (getApiErrorStatus(apiError) === 409) { // The node was already configured (e.g. session still had setup_in_progress set // after a previous successful setup). Clean up and redirect to login. sessionStorage.removeItem("setup_in_progress") diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils.ts index 62363553bd..57a2aebf99 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils.ts @@ -1,25 +1,26 @@ -import { AxiosError } from "axios" -import type { ApiError } from "./client" +import type { ApiError } from "@/lib/api-error" +import { getApiErrorResponseBody, isApiError } from "@/lib/api-error" export function extractErrorMessage(err: ApiError): string { - if (err instanceof AxiosError) { + if (isApiError(err)) { + const body = getApiErrorResponseBody(err) + const errDetail = (body as { detail?: unknown })?.detail + if (Array.isArray(errDetail) && errDetail.length > 0) { + const first = errDetail[0] as { msg?: string } + return first.msg ?? err.message + } + if (errDetail && typeof errDetail === "object") { + const message = (errDetail as { message?: unknown }).message + if (typeof message === "string" && message.length > 0) { + return message + } + } + if (typeof errDetail === "string") { + return errDetail + } return err.message } - const errDetail = (err.body as { detail?: unknown })?.detail - if (Array.isArray(errDetail) && errDetail.length > 0) { - const first = errDetail[0] as { msg?: string } - return first.msg ?? "Something went wrong." - } - if (errDetail && typeof errDetail === "object") { - const message = (errDetail as { message?: unknown }).message - if (typeof message === "string" && message.length > 0) { - return message - } - } - if (typeof errDetail === "string") { - return errDetail - } return "Something went wrong." } diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-errors.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-errors.test.ts index e3dc85c542..a20fe90623 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-errors.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-errors.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { formatTaskErrorDisplay, resolveTaskError } from "@/utils/task-errors" function makeTask(overrides: Partial = {}): Task { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-status.test.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-status.test.ts index 63a2d4df6a..bab9e50204 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-status.test.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/__tests__/task-status.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { getTaskFilterGroup, getTaskMetaStatus } from "@/utils/task-status" function taskWithExecStatus( diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-errors.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-errors.ts index 4f2f88b99e..e2b4a50150 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-errors.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-errors.ts @@ -1,4 +1,4 @@ -import type { Task_Output as Task } from "@/client" +import type { TaskOutput as Task } from "@/client" import { getActiveExecution } from "@/utils/executions" export interface TaskErrorInfo { diff --git a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-status.ts b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-status.ts index ebd748b095..f5413b35be 100644 --- a/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-status.ts +++ b/packages/tentacles/Services/Interfaces/node_web_interface/src/utils/task-status.ts @@ -1,4 +1,4 @@ -import type { Task_Output as Task, TaskStatus } from "@/client" +import type { TaskOutput as Task, TaskStatus } from "@/client" import { getActiveExecution, getStatusGroup } from "@/utils/executions" export type TaskFilterGroup = "active" | "completed" | "errored"