From 45923b4bd2976a3a50b5c49c9c39f94812885fca Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Tue, 14 Apr 2026 14:42:33 -0500 Subject: [PATCH 1/3] feat: Organize tools under admin folder with environment variable support Reorganizes all MCP admin tools under src/tools/admin/ structure: - admin/users/: User lifecycle, OAuth credentials, CSV bulk operations - admin/groups/: Group/group set management, membership operations - admin/content/: Projects, workbooks, views operations - admin/jobs/: Background job monitoring - admin/operations/: Job conflict detection, workbook archiving - admin/permissions/: Content permission management Adds three tool groups (admin, content, operations) that work with INCLUDE_TOOLS and EXCLUDE_TOOLS environment variables for granular access control. Tools can be filtered by individual name or group name. --- package-lock.json | 2329 ++++++++++++++--- package.json | 3 +- src/sdks/tableau/apis/adminApi.ts | 291 ++ src/sdks/tableau/apis/jobsApi.ts | 14 + src/sdks/tableau/apis/permissionsApi.ts | 17 + src/sdks/tableau/apis/projectsApi.ts | 114 + src/sdks/tableau/methods/adminMethods.ts | 254 ++ src/sdks/tableau/methods/jobsMethods.ts | 49 + .../tableau/methods/permissionsMethods.ts | 232 ++ src/sdks/tableau/methods/projectsMethods.ts | 120 + src/tools/admin/content/contentProjects.ts | 123 + src/tools/admin/content/contentViews.ts | 213 ++ src/tools/admin/content/contentWorkbooks.ts | 179 ++ src/tools/admin/groups/adminGroups.ts | 236 ++ src/tools/admin/jobs/siteJobs.ts | 87 + .../admin/operations/tableauOperations.ts | 636 +++++ .../operations/tableauOperationsHelpers.ts | 322 +++ .../admin/permissions/contentPermissions.ts | 212 ++ src/tools/admin/users/adminUsers.ts | 189 ++ src/tools/toolName.ts | 51 +- src/tools/tools.ts | 50 +- src/utils/uploadWorkbookToS3.ts | 24 + 22 files changed, 5329 insertions(+), 416 deletions(-) create mode 100644 src/sdks/tableau/apis/adminApi.ts create mode 100644 src/sdks/tableau/apis/jobsApi.ts create mode 100644 src/sdks/tableau/apis/permissionsApi.ts create mode 100644 src/sdks/tableau/apis/projectsApi.ts create mode 100644 src/sdks/tableau/methods/adminMethods.ts create mode 100644 src/sdks/tableau/methods/jobsMethods.ts create mode 100644 src/sdks/tableau/methods/permissionsMethods.ts create mode 100644 src/sdks/tableau/methods/projectsMethods.ts create mode 100644 src/tools/admin/content/contentProjects.ts create mode 100644 src/tools/admin/content/contentViews.ts create mode 100644 src/tools/admin/content/contentWorkbooks.ts create mode 100644 src/tools/admin/groups/adminGroups.ts create mode 100644 src/tools/admin/jobs/siteJobs.ts create mode 100644 src/tools/admin/operations/tableauOperations.ts create mode 100644 src/tools/admin/operations/tableauOperationsHelpers.ts create mode 100644 src/tools/admin/permissions/contentPermissions.ts create mode 100644 src/tools/admin/users/adminUsers.ts create mode 100644 src/utils/uploadWorkbookToS3.ts diff --git a/package-lock.json b/package-lock.json index 5d03f431b..31961c963 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@tableau/mcp-server", - "version": "1.18.3", + "version": "1.18.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tableau/mcp-server", - "version": "1.18.3", + "version": "1.18.2", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/client-s3": "^3.1030.0", "@modelcontextprotocol/sdk": "^1.26.0", "@zodios/core": "^10.9.6", "cookie-parser": "^1.4.7", @@ -103,296 +104,1147 @@ "node": ">= 4" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "engines": { - "node": ">=6.9.0" + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", - "dev": true, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", "dependencies": { - "@babel/types": "^7.27.3" + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "bin": { - "parser": "bin/babel-parser.js" + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", - "dev": true, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=16.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@aws-sdk/client-s3": { + "version": "3.1030.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1030.0.tgz", + "integrity": "sha512-sgGb4ub0JXnHaXnok5td7A1KGwENFPwOrwgzvpkeWq9w16Sl7x2KhYtVl+Fdd/7LAvaEtm3HqrYtNmm2d0OXmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-node": "^3.972.30", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.9", + "@aws-sdk/middleware-expect-continue": "^3.972.9", + "@aws-sdk/middleware-flexible-checksums": "^3.974.7", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-location-constraint": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-sdk-s3": "^3.972.28", + "@aws-sdk/middleware-ssec": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.16", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/eventstream-serde-browser": "^4.2.13", + "@smithy/eventstream-serde-config-resolver": "^4.3.13", + "@smithy/eventstream-serde-node": "^4.2.13", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-blob-browser": "^4.2.14", + "@smithy/hash-node": "^4.2.13", + "@smithy/hash-stream-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/md5-js": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.15", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", + "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/xml-builder": "^3.972.17", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.6.tgz", + "integrity": "sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", + "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", + "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", + "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-login": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", + "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", + "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-ini": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", + "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", + "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/token-providers": "3.1026.0", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", + "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.9.tgz", + "integrity": "sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.9.tgz", + "integrity": "sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.7.tgz", + "integrity": "sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/crc64-nvme": "^3.972.6", + "@aws-sdk/types": "^3.973.7", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", + "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.9.tgz", + "integrity": "sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", + "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", + "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.28.tgz", + "integrity": "sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.9.tgz", + "integrity": "sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", + "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-retry": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", + "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", + "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/config-resolver": "^4.4.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.16.tgz", + "integrity": "sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.28", + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", + "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", + "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", + "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", + "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", + "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "fast-xml-parser": "5.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-loong64": { + "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ - "loong64" + "ppc64" ], "dev": true, "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-mips64el": { + "node_modules/@esbuild/android-arm": { "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ - "mips64el" + "arm" ], "dev": true, "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-ppc64": { + "node_modules/@esbuild/android-arm64": { "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { + "node_modules/@esbuild/android-x64": { "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "optional": true, "os": [ - "linux" + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" ], "engines": { "node": ">=18" @@ -1568,159 +2420,878 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.15", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.15.tgz", + "integrity": "sha512-BJdMBY5YO9iHh+lPLYdHv6LbX+J8IcPCYMl1IJdBt2KDWNHwONHrPVHk3ttYBqJd9wxv84wlbN0f7GlQzcQtNQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.0", + "@smithy/util-middleware": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.14", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", + "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", + "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz", + "integrity": "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.0", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz", + "integrity": "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz", + "integrity": "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz", + "integrity": "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz", + "integrity": "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", + "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.14.tgz", + "integrity": "sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", + "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.13.tgz", + "integrity": "sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", + "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.13.tgz", + "integrity": "sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", + "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", + "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-middleware": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.1.tgz", + "integrity": "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.1", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", + "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", + "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", + "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", + "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", + "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", + "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", + "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", + "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", + "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", + "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", + "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", + "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", + "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", + "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", + "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.50", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.50.tgz", + "integrity": "sha512-xpjncL5XozFA3No7WypTsPU1du0fFS8flIyO+Wh2nhCy7bpEapvU7BR55Bg+wrfw+1cRA+8G8UsTjaxgzrMzXg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.15", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@smithy/util-endpoints": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.0.tgz", + "integrity": "sha512-QQHGPKkw6NPcU6TJ1rNEEa201srPtZiX4k61xL163vvs9sTqW/XKz+UEuJ00uvPqoN+5Rs4Ka1UJ7+Mp03IXJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@smithy/util-middleware": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", + "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "node_modules/@smithy/util-retry": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.1.tgz", + "integrity": "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "node_modules/@smithy/util-stream": { + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/util-waiter": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.15.tgz", + "integrity": "sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, "node_modules/@types/body-parser": { "version": "1.19.6", @@ -2649,6 +4220,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3790,6 +5367,41 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -5719,6 +7331,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6813,6 +8440,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/superagent": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", @@ -7109,6 +8748,12 @@ "resolved": "https://registry.npmjs.org/ts-results-es/-/ts-results-es-5.0.1.tgz", "integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", diff --git a/package.json b/package.json index 344137080..55410363e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tableau/mcp-server", "description": "Helping agents see and understand data.", - "version": "1.18.3", + "version": "1.18.2", "repository": { "type": "git", "url": "git+https://github.com/tableau/tableau-mcp.git" @@ -54,6 +54,7 @@ "docs:start": "npm run start --prefix docs" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1030.0", "@modelcontextprotocol/sdk": "^1.26.0", "@zodios/core": "^10.9.6", "cookie-parser": "^1.4.7", diff --git a/src/sdks/tableau/apis/adminApi.ts b/src/sdks/tableau/apis/adminApi.ts new file mode 100644 index 000000000..23063ee81 --- /dev/null +++ b/src/sdks/tableau/apis/adminApi.ts @@ -0,0 +1,291 @@ +import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core'; +import { z } from 'zod'; + +import { paginationParameters } from './paginationParameters.js'; + +const anyResponse = z.any(); +const siteIdPathParameter = { + name: 'siteId', + type: 'Path' as const, + schema: z.string(), +}; +const groupIdPathParameter = { + name: 'groupId', + type: 'Path' as const, + schema: z.string(), +}; +const groupSetIdPathParameter = { + name: 'groupSetId', + type: 'Path' as const, + schema: z.string(), +}; +const userIdPathParameter = { + name: 'userId', + type: 'Path' as const, + schema: z.string(), +}; + +const adminApi = makeApi([ + makeEndpoint({ + method: 'put', + path: '/sites/:siteId/groupsets/:groupSetId/groups/:groupId', + alias: 'addGroupToGroupSet', + description: 'Adds group to a group set.', + parameters: [siteIdPathParameter, groupSetIdPathParameter, groupIdPathParameter], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/groups/:groupId/users', + alias: 'addUserToGroup', + description: 'Adds one or more users to a group.', + parameters: [ + siteIdPathParameter, + groupIdPathParameter, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/users', + alias: 'addUserToSite', + description: 'Adds a user to a site.', + parameters: [siteIdPathParameter, { name: 'body', type: 'Body', schema: z.any() }], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/groups', + alias: 'createGroup', + description: 'Creates a group on a site.', + parameters: [ + siteIdPathParameter, + { + name: 'asJob', + type: 'Query', + schema: z.boolean().optional(), + }, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/groupsets', + alias: 'createGroupSet', + description: 'Creates a group set.', + parameters: [siteIdPathParameter, { name: 'body', type: 'Body', schema: z.any() }], + response: anyResponse, + }), + makeEndpoint({ + method: 'delete', + path: '/sites/:siteId/groups/:groupId', + alias: 'deleteGroup', + description: 'Deletes a group.', + parameters: [siteIdPathParameter, groupIdPathParameter], + response: anyResponse, + }), + makeEndpoint({ + method: 'delete', + path: '/sites/:siteId/groupsets/:groupSetId', + alias: 'deleteGroupSet', + description: 'Deletes a group set.', + parameters: [siteIdPathParameter, groupSetIdPathParameter], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/users/delete', + alias: 'deleteUsersFromSiteWithCsv', + description: 'Deletes users from site via CSV multipart upload.', + parameters: [siteIdPathParameter, { name: 'body', type: 'Body', schema: z.any() }], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/users/:userId/retrieveSavedCreds', + alias: 'downloadUserCredentials', + description: 'Downloads user credentials for migration.', + parameters: [ + siteIdPathParameter, + userIdPathParameter, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/users/:userId/groups', + alias: 'getGroupsForUser', + description: 'Gets groups for a user.', + parameters: [siteIdPathParameter, userIdPathParameter, ...paginationParameters], + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/groupsets/:groupSetId', + alias: 'getGroupSet', + description: 'Gets a group set.', + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/groups/:groupId/users', + alias: 'getUsersInGroup', + description: 'Gets users in a group.', + parameters: [siteIdPathParameter, groupIdPathParameter, ...paginationParameters], + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/users', + alias: 'getUsersOnSite', + description: 'Gets users on a site.', + parameters: [ + siteIdPathParameter, + ...paginationParameters, + { name: 'filter', type: 'Query', schema: z.string().optional() }, + { name: 'sort', type: 'Query', schema: z.string().optional() }, + { name: 'fields', type: 'Query', schema: z.string().optional() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'post', + path: '/sites/:siteId/users/import', + alias: 'importUsersToSiteFromCsv', + description: 'Imports users to site from CSV multipart upload.', + parameters: [ + siteIdPathParameter, + { name: 'isVerbose', type: 'Query', schema: z.boolean().optional() }, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/groupsets', + alias: 'listGroupSets', + description: 'Lists group sets.', + parameters: [ + siteIdPathParameter, + ...paginationParameters, + { name: 'filter', type: 'Query', schema: z.string().optional() }, + { name: 'sort', type: 'Query', schema: z.string().optional() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/groups', + alias: 'queryGroups', + description: 'Queries groups.', + parameters: [ + siteIdPathParameter, + ...paginationParameters, + { name: 'filter', type: 'Query', schema: z.string().optional() }, + { name: 'sort', type: 'Query', schema: z.string().optional() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'get', + path: '/sites/:siteId/users/:userId', + alias: 'queryUserOnSite', + description: 'Queries a user on site.', + parameters: [siteIdPathParameter, userIdPathParameter], + response: anyResponse, + }), + makeEndpoint({ + method: 'delete', + path: '/sites/:siteId/groupsets/:groupSetId/groups/:groupId', + alias: 'removeGroupFromGroupSet', + description: 'Removes a group from group set.', + parameters: [siteIdPathParameter, groupSetIdPathParameter, groupIdPathParameter], + response: anyResponse, + }), + makeEndpoint({ + method: 'delete', + path: '/sites/:siteId/users/:userId', + alias: 'removeUserFromSite', + description: 'Removes user from site.', + parameters: [ + siteIdPathParameter, + userIdPathParameter, + { name: 'mapAssetsTo', type: 'Query', schema: z.string().optional() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'delete', + path: '/sites/:siteId/groups/:groupId/users/:userId', + alias: 'removeUserFromGroup', + description: 'Removes one user from group.', + parameters: [siteIdPathParameter, groupIdPathParameter, userIdPathParameter], + response: anyResponse, + }), + makeEndpoint({ + method: 'put', + path: '/sites/:siteId/groups/:groupId/users/remove', + alias: 'bulkRemoveUsersFromGroup', + description: 'Bulk removes users from group.', + parameters: [ + siteIdPathParameter, + groupIdPathParameter, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'put', + path: '/sites/:siteId/groups/:groupId', + alias: 'updateGroup', + description: 'Updates a group.', + parameters: [ + siteIdPathParameter, + groupIdPathParameter, + { name: 'asJob', type: 'Query', schema: z.boolean().optional() }, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'put', + path: '/sites/:siteId/groupsets/:groupSetId', + alias: 'updateGroupSet', + description: 'Updates a group set.', + parameters: [ + siteIdPathParameter, + groupSetIdPathParameter, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'put', + path: '/sites/:siteId/users/:userId', + alias: 'updateUser', + description: 'Updates a user.', + parameters: [ + siteIdPathParameter, + userIdPathParameter, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), + makeEndpoint({ + method: 'put', + path: '/sites/:siteId/users/:userId/uploadSavedCreds', + alias: 'uploadUserCredentials', + description: 'Uploads user credentials to destination site.', + parameters: [ + siteIdPathParameter, + userIdPathParameter, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, + }), +]); + +export const adminApis = [...adminApi] as const satisfies ZodiosEndpointDefinitions; diff --git a/src/sdks/tableau/apis/jobsApi.ts b/src/sdks/tableau/apis/jobsApi.ts new file mode 100644 index 000000000..ac9d7af2a --- /dev/null +++ b/src/sdks/tableau/apis/jobsApi.ts @@ -0,0 +1,14 @@ +import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core'; +import { z } from 'zod'; + +const jobsPlaceholderEndpoint = makeEndpoint({ + method: 'get', + path: '/__jobs_sdk_placeholder', + alias: 'jobsPlaceholder', + description: 'Unused placeholder so Zodios can construct a client.', + response: z.any(), + parameters: [], +}); + +const jobsApi = makeApi([jobsPlaceholderEndpoint]); +export const jobsApis = [...jobsApi] as const satisfies ZodiosEndpointDefinitions; diff --git a/src/sdks/tableau/apis/permissionsApi.ts b/src/sdks/tableau/apis/permissionsApi.ts new file mode 100644 index 000000000..eab9bbb47 --- /dev/null +++ b/src/sdks/tableau/apis/permissionsApi.ts @@ -0,0 +1,17 @@ +import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core'; +import { z } from 'zod'; + +/** + * Zodios requires at least one endpoint; permissions use raw axios paths in PermissionsMethods. + */ +const permissionsPlaceholderEndpoint = makeEndpoint({ + method: 'get', + path: '/__permissions_sdk_placeholder', + alias: 'permissionsPlaceholder', + description: 'Unused placeholder so Zodios can construct a client.', + response: z.any(), + parameters: [], +}); + +const permissionsApi = makeApi([permissionsPlaceholderEndpoint]); +export const permissionsApis = [...permissionsApi] as const satisfies ZodiosEndpointDefinitions; diff --git a/src/sdks/tableau/apis/projectsApi.ts b/src/sdks/tableau/apis/projectsApi.ts new file mode 100644 index 000000000..1bed4f988 --- /dev/null +++ b/src/sdks/tableau/apis/projectsApi.ts @@ -0,0 +1,114 @@ +import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core'; +import { z } from 'zod'; + +import { paginationSchema } from '../types/pagination.js'; +import { projectSchema } from '../types/project.js'; +import { paginationParameters } from './paginationParameters.js'; + +const anyResponse = z.any(); + +const queryProjectsEndpoint = makeEndpoint({ + method: 'get', + path: '/sites/:siteId/projects', + alias: 'queryProjects', + description: 'Returns a list of projects on the specified site.', + parameters: [ + ...paginationParameters, + { + name: 'siteId', + type: 'Path', + schema: z.string(), + }, + { + name: 'filter', + type: 'Query', + schema: z.string().optional(), + description: + 'An expression that lets you specify a subset of projects to return. You can filter on predefined fields such as name, createdAt, and updatedAt.', + }, + { + name: 'sort', + type: 'Query', + schema: z.string().optional(), + description: 'Sort expression for the result set.', + }, + { + name: 'fields', + type: 'Query', + schema: z.string().optional(), + description: 'Comma-separated list of fields to include in the response.', + }, + ], + response: z.object({ + pagination: paginationSchema, + projects: z.object({ + project: z.array(projectSchema).optional(), + }), + }), +}); + +const createProjectEndpoint = makeEndpoint({ + method: 'post', + path: '/sites/:siteId/projects', + alias: 'createProject', + description: 'Creates a project on the specified site.', + parameters: [ + { + name: 'siteId', + type: 'Path', + schema: z.string(), + }, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, +}); + +const updateProjectEndpoint = makeEndpoint({ + method: 'put', + path: '/sites/:siteId/projects/:projectId', + alias: 'updateProject', + description: 'Updates the specified project.', + parameters: [ + { + name: 'siteId', + type: 'Path', + schema: z.string(), + }, + { + name: 'projectId', + type: 'Path', + schema: z.string(), + }, + { name: 'body', type: 'Body', schema: z.any() }, + ], + response: anyResponse, +}); + +const deleteProjectEndpoint = makeEndpoint({ + method: 'delete', + path: '/sites/:siteId/projects/:projectId', + alias: 'deleteProject', + description: 'Deletes the specified project.', + parameters: [ + { + name: 'siteId', + type: 'Path', + schema: z.string(), + }, + { + name: 'projectId', + type: 'Path', + schema: z.string(), + }, + ], + response: anyResponse, +}); + +const projectsApi = makeApi([ + queryProjectsEndpoint, + createProjectEndpoint, + updateProjectEndpoint, + deleteProjectEndpoint, +]); + +export const projectsApis = [...projectsApi] as const satisfies ZodiosEndpointDefinitions; diff --git a/src/sdks/tableau/methods/adminMethods.ts b/src/sdks/tableau/methods/adminMethods.ts new file mode 100644 index 000000000..b54a2af52 --- /dev/null +++ b/src/sdks/tableau/methods/adminMethods.ts @@ -0,0 +1,254 @@ +import { Zodios } from '@zodios/core'; + +import { AxiosRequestConfig } from '../../../utils/axios.js'; +import { adminApis } from '../apis/adminApi.js'; +import { Credentials } from '../types/credentials.js'; +import AuthenticatedMethods from './authenticatedMethods.js'; + +type PagingQuery = { + pageSize?: number; + pageNumber?: number; +}; + +export default class AdminMethods extends AuthenticatedMethods { + constructor(baseUrl: string, creds: Credentials, axiosConfig: AxiosRequestConfig) { + super(new Zodios(baseUrl, adminApis, { axiosConfig }), creds); + } + + addGroupToGroupSet = async ( + siteId: string, + groupSetId: string, + groupId: string, + ): Promise => + ( + await this._apiClient.axios.put( + `/sites/${siteId}/groupsets/${groupSetId}/groups/${groupId}`, + undefined, + { ...this.authHeader }, + ) + ).data; + + addUserToGroup = async (siteId: string, groupId: string, body: unknown): Promise => + ( + await this._apiClient.axios.post( + `/sites/${siteId}/groups/${groupId}/users`, + body, + this.authHeader, + ) + ).data; + + addUserToSite = async (siteId: string, body: unknown): Promise => + await (async () => { + const url = `/sites/${siteId}/users`; + console.warn('Constructing URL:', { + method: 'POST', + operation: 'add-user-to-site', + siteId, + url, + }); + const response = await this._apiClient.axios.post(url, body, { + ...this.authHeader, + }); + return response.data; + })(); + + createGroup = async ( + siteId: string, + body: unknown, + queries?: { asJob?: boolean }, + ): Promise => + ( + await this._apiClient.axios.post(`/sites/${siteId}/groups`, body, { + ...this.authHeader, + params: queries, + }) + ).data; + + createGroupSet = async (siteId: string, body: unknown): Promise => + (await this._apiClient.axios.post(`/sites/${siteId}/groupsets`, body, this.authHeader)).data; + + deleteGroup = async (siteId: string, groupId: string): Promise => + (await this._apiClient.axios.delete(`/sites/${siteId}/groups/${groupId}`, this.authHeader)) + .data; + + deleteGroupSet = async (siteId: string, groupSetId: string): Promise => + ( + await this._apiClient.axios.delete( + `/sites/${siteId}/groupsets/${groupSetId}`, + this.authHeader, + ) + ).data; + + deleteUsersFromSiteWithCsv = async (siteId: string, body: unknown): Promise => + (await this._apiClient.axios.post(`/sites/${siteId}/users/delete`, body, this.authHeader)).data; + + downloadUserCredentials = async ( + siteId: string, + userId: string, + body: unknown, + ): Promise => + ( + await this._apiClient.axios.post( + `/sites/${siteId}/users/${userId}/retrieveSavedCreds`, + body, + this.authHeader, + ) + ).data; + + getGroupsForUser = async ( + siteId: string, + userId: string, + queries?: PagingQuery, + ): Promise => + ( + await this._apiClient.axios.get(`/sites/${siteId}/users/${userId}/groups`, { + ...this.authHeader, + params: queries, + }) + ).data; + + getGroupSet = async (siteId: string, groupSetId: string): Promise => + (await this._apiClient.axios.get(`/sites/${siteId}/groupsets/${groupSetId}`, this.authHeader)) + .data; + + getUsersInGroup = async ( + siteId: string, + groupId: string, + queries?: PagingQuery, + ): Promise => + ( + await this._apiClient.axios.get(`/sites/${siteId}/groups/${groupId}/users`, { + ...this.authHeader, + params: queries, + }) + ).data; + + getUsersOnSite = async ( + siteId: string, + queries?: PagingQuery & { filter?: string; sort?: string; fields?: string }, + ): Promise => + ( + await this._apiClient.axios.get(`/sites/${siteId}/users`, { + ...this.authHeader, + params: queries, + }) + ).data; + + importUsersToSiteFromCsv = async ( + siteId: string, + body: unknown, + queries?: { isVerbose?: boolean }, + ): Promise => + ( + await this._apiClient.axios.post(`/sites/${siteId}/users/import`, body, { + ...this.authHeader, + params: queries, + }) + ).data; + + listGroupSets = async ( + siteId: string, + queries?: PagingQuery & { filter?: string; sort?: string }, + ): Promise => + ( + await this._apiClient.axios.get(`/sites/${siteId}/groupsets`, { + ...this.authHeader, + params: queries, + }) + ).data; + + queryGroups = async ( + siteId: string, + queries?: PagingQuery & { filter?: string; sort?: string }, + ): Promise => + ( + await this._apiClient.axios.get(`/sites/${siteId}/groups`, { + ...this.authHeader, + params: queries, + }) + ).data; + + queryUserOnSite = async (siteId: string, userId: string): Promise => + (await this._apiClient.axios.get(`/sites/${siteId}/users/${userId}`, this.authHeader)).data; + + removeGroupFromGroupSet = async ( + siteId: string, + groupSetId: string, + groupId: string, + ): Promise => + ( + await this._apiClient.axios.delete( + `/sites/${siteId}/groupsets/${groupSetId}/groups/${groupId}`, + this.authHeader, + ) + ).data; + + removeUserFromSite = async ( + siteId: string, + userId: string, + queries?: { mapAssetsTo?: string }, + ): Promise => + ( + await this._apiClient.axios.delete(`/sites/${siteId}/users/${userId}`, { + ...this.authHeader, + params: queries, + }) + ).data; + + removeUserFromGroup = async (siteId: string, groupId: string, userId: string): Promise => + ( + await this._apiClient.axios.delete( + `/sites/${siteId}/groups/${groupId}/users/${userId}`, + this.authHeader, + ) + ).data; + + bulkRemoveUsersFromGroup = async ( + siteId: string, + groupId: string, + body: unknown, + ): Promise => + ( + await this._apiClient.axios.put( + `/sites/${siteId}/groups/${groupId}/users/remove`, + body, + this.authHeader, + ) + ).data; + + updateGroup = async ( + siteId: string, + groupId: string, + body: unknown, + queries?: { asJob?: boolean }, + ): Promise => + ( + await this._apiClient.axios.put(`/sites/${siteId}/groups/${groupId}`, body, { + ...this.authHeader, + params: queries, + }) + ).data; + + updateGroupSet = async (siteId: string, groupSetId: string, body: unknown): Promise => + ( + await this._apiClient.axios.put( + `/sites/${siteId}/groupsets/${groupSetId}`, + body, + this.authHeader, + ) + ).data; + + updateUser = async (siteId: string, userId: string, body: unknown): Promise => + ( + await this._apiClient.axios.put(`/sites/${siteId}/users/${userId}`, body, { + ...this.authHeader, + }) + ).data; + + uploadUserCredentials = async (siteId: string, userId: string, body: unknown): Promise => + ( + await this._apiClient.axios.put(`/sites/${siteId}/users/${userId}/uploadSavedCreds`, body, { + ...this.authHeader, + }) + ).data; +} diff --git a/src/sdks/tableau/methods/jobsMethods.ts b/src/sdks/tableau/methods/jobsMethods.ts new file mode 100644 index 000000000..3b9a2dd1d --- /dev/null +++ b/src/sdks/tableau/methods/jobsMethods.ts @@ -0,0 +1,49 @@ +import { Zodios } from '@zodios/core'; + +import { AxiosRequestConfig } from '../../../utils/axios.js'; +import { jobsApis } from '../apis/jobsApi.js'; +import { Credentials } from '../types/credentials.js'; +import AuthenticatedMethods from './authenticatedMethods.js'; + +/** + * Jobs — Query Jobs, Query Job, Cancel Job (PUT). + * + * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm + */ +export default class JobsMethods extends AuthenticatedMethods { + constructor(baseUrl: string, creds: Credentials, axiosConfig: AxiosRequestConfig) { + super(new Zodios(baseUrl, jobsApis, { axiosConfig }), creds); + } + + private jsonHeaders(): AxiosRequestConfig { + return { + ...this.authHeader, + headers: { + ...this.authHeader.headers, + Accept: 'application/json', + }, + }; + } + + queryJobs = async ( + siteId: string, + queries?: { filter?: string; pageSize?: number; pageNumber?: number }, + ): Promise => + ( + await this._apiClient.axios.get(`/sites/${siteId}/jobs`, { + ...this.jsonHeaders(), + params: queries, + }) + ).data; + + queryJob = async (siteId: string, jobId: string): Promise => + (await this._apiClient.axios.get(`/sites/${siteId}/jobs/${jobId}`, this.jsonHeaders())).data; + + /** Cancel Job — HTTP PUT (not DELETE). */ + cancelJob = async (siteId: string, jobId: string): Promise => + ( + await this._apiClient.axios.put(`/sites/${siteId}/jobs/${jobId}`, undefined, { + ...this.jsonHeaders(), + }) + ).data; +} diff --git a/src/sdks/tableau/methods/permissionsMethods.ts b/src/sdks/tableau/methods/permissionsMethods.ts new file mode 100644 index 000000000..75cb157a0 --- /dev/null +++ b/src/sdks/tableau/methods/permissionsMethods.ts @@ -0,0 +1,232 @@ +import { Zodios } from '@zodios/core'; + +import { AxiosRequestConfig } from '../../../utils/axios.js'; +import { permissionsApis } from '../apis/permissionsApi.js'; +import { Credentials } from '../types/credentials.js'; +import AuthenticatedMethods from './authenticatedMethods.js'; + +/** Granular content types (Ask Data / lenses excluded). */ +export type GranularPermissionKind = + | 'collection' + | 'datasource' + | 'project' + | 'view' + | 'virtualconnection' + | 'workbook'; + +/** Default permission URI segments under a project (lenses excluded — Ask Data). */ +export type DefaultPermissionSegment = + | 'workbooks' + | 'datasources' + | 'dataroles' + | 'metrics' + | 'flows' + | 'virtualconnections' + | 'databases' + | 'tables'; + +/** POST replace-project-default-permissions resource segments (per REST API 3.23+). */ +export type ReplaceProjectDefaultSegment = + | 'dataroles' + | 'databases' + | 'datasources' + | 'flows' + | 'tables' + | 'workbooks'; + +/** POST replace-content-permissions targets. */ +export type ReplaceContentKind = 'datasource' | 'flow' | 'project' | 'view' | 'workbook'; + +export type GranteePathKind = 'users' | 'groups'; + +function granularPermissionsPath( + siteId: string, + kind: GranularPermissionKind, + resourceId: string, +): string { + switch (kind) { + case 'collection': + return `/sites/${siteId}/collections/${resourceId}/permissions`; + case 'datasource': + return `/sites/${siteId}/datasources/${resourceId}/permissions`; + case 'project': + return `/sites/${siteId}/projects/${resourceId}/permissions`; + case 'view': + return `/sites/${siteId}/views/${resourceId}/permissions`; + case 'virtualconnection': + return `/sites/${siteId}/virtualconnections/${resourceId}/permissions`; + case 'workbook': + return `/sites/${siteId}/workbooks/${resourceId}/permissions`; + default: { + const _exhaustive: never = kind; + return _exhaustive; + } + } +} + +function defaultPermissionsBasePath( + siteId: string, + projectId: string, + segment: DefaultPermissionSegment, +): string { + return `/sites/${siteId}/projects/${projectId}/default-permissions/${segment}`; +} + +function replaceContentPath(siteId: string, kind: ReplaceContentKind, resourceId: string): string { + switch (kind) { + case 'datasource': + return `/sites/${siteId}/datasources/${resourceId}/permissions`; + case 'flow': + return `/sites/${siteId}/flows/${resourceId}/permissions`; + case 'project': + return `/sites/${siteId}/projects/${resourceId}/permissions`; + case 'view': + return `/sites/${siteId}/views/${resourceId}/permissions`; + case 'workbook': + return `/sites/${siteId}/workbooks/${resourceId}/permissions`; + default: { + const _exhaustive: never = kind; + return _exhaustive; + } + } +} + +/** + * Tableau REST API — Permissions Methods (Ask Data / lens endpoints omitted). + * + * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm + */ +export default class PermissionsMethods extends AuthenticatedMethods { + constructor(baseUrl: string, creds: Credentials, axiosConfig: AxiosRequestConfig) { + super(new Zodios(baseUrl, permissionsApis, { axiosConfig }), creds); + } + + private jsonHeaders(): AxiosRequestConfig { + return { + ...this.authHeader, + headers: { + ...this.authHeader.headers, + Accept: 'application/json', + }, + }; + } + + private jsonWriteHeaders(): AxiosRequestConfig { + return { + ...this.authHeader, + headers: { + ...this.authHeader.headers, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }; + } + + listGranularPermissions = async ( + siteId: string, + kind: GranularPermissionKind, + resourceId: string, + ): Promise => + ( + await this._apiClient.axios.get( + granularPermissionsPath(siteId, kind, resourceId), + this.jsonHeaders(), + ) + ).data; + + addGranularPermissions = async ( + siteId: string, + kind: GranularPermissionKind, + resourceId: string, + body: unknown, + ): Promise => + ( + await this._apiClient.axios.put( + granularPermissionsPath(siteId, kind, resourceId), + body, + this.jsonWriteHeaders(), + ) + ).data; + + deleteGranularPermission = async ( + siteId: string, + kind: GranularPermissionKind, + resourceId: string, + grantee: GranteePathKind, + granteeId: string, + capabilityName: string, + capabilityMode: string, + ): Promise => { + const base = granularPermissionsPath(siteId, kind, resourceId); + const path = `${base}/${grantee}/${granteeId}/${encodeURIComponent(capabilityName)}/${encodeURIComponent(capabilityMode)}`; + return (await this._apiClient.axios.delete(path, this.jsonHeaders())).data; + }; + + listDefaultPermissions = async ( + siteId: string, + projectId: string, + segment: DefaultPermissionSegment, + ): Promise => + ( + await this._apiClient.axios.get( + defaultPermissionsBasePath(siteId, projectId, segment), + this.jsonHeaders(), + ) + ).data; + + addDefaultPermissions = async ( + siteId: string, + projectId: string, + segment: DefaultPermissionSegment, + body: unknown, + ): Promise => + ( + await this._apiClient.axios.put( + defaultPermissionsBasePath(siteId, projectId, segment), + body, + this.jsonWriteHeaders(), + ) + ).data; + + deleteDefaultPermission = async ( + siteId: string, + projectId: string, + segment: DefaultPermissionSegment, + grantee: GranteePathKind, + granteeId: string, + capabilityName: string, + capabilityMode: string, + ): Promise => { + const base = defaultPermissionsBasePath(siteId, projectId, segment); + const path = `${base}/${grantee}/${granteeId}/${encodeURIComponent(capabilityName)}/${encodeURIComponent(capabilityMode)}`; + return (await this._apiClient.axios.delete(path, this.jsonHeaders())).data; + }; + + replaceProjectDefaultPermissions = async ( + siteId: string, + projectId: string, + segment: ReplaceProjectDefaultSegment, + body: unknown, + ): Promise => + ( + await this._apiClient.axios.post( + `/sites/${siteId}/projects/${projectId}/default-permissions/${segment}`, + body, + this.jsonWriteHeaders(), + ) + ).data; + + replaceContentPermissions = async ( + siteId: string, + kind: ReplaceContentKind, + resourceId: string, + body: unknown, + ): Promise => + ( + await this._apiClient.axios.post( + replaceContentPath(siteId, kind, resourceId), + body, + this.jsonWriteHeaders(), + ) + ).data; +} diff --git a/src/sdks/tableau/methods/projectsMethods.ts b/src/sdks/tableau/methods/projectsMethods.ts new file mode 100644 index 000000000..6871218eb --- /dev/null +++ b/src/sdks/tableau/methods/projectsMethods.ts @@ -0,0 +1,120 @@ +import { Zodios } from '@zodios/core'; + +import { AxiosRequestConfig } from '../../../utils/axios.js'; +import { projectsApis } from '../apis/projectsApi.js'; +import { Credentials } from '../types/credentials.js'; +import { Pagination } from '../types/pagination.js'; +import { Project } from '../types/project.js'; +import AuthenticatedMethods from './authenticatedMethods.js'; + +/** + * Projects methods of the Tableau Server REST API + * + * @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_projects.htm + */ +export default class ProjectsMethods extends AuthenticatedMethods { + constructor(baseUrl: string, creds: Credentials, axiosConfig: AxiosRequestConfig) { + super(new Zodios(baseUrl, projectsApis, { axiosConfig }), creds); + } + + private jsonHeaders(): AxiosRequestConfig { + return { + ...this.authHeader, + headers: { + ...this.authHeader.headers, + Accept: 'application/json', + }, + }; + } + + private jsonWriteHeaders(): AxiosRequestConfig { + return { + ...this.authHeader, + headers: { + ...this.authHeader.headers, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }; + } + + /** + * Returns a list of projects on the specified site. + * + * Required scopes: `tableau:content:read` + */ + queryProjects = async ({ + siteId, + filter, + sort, + pageSize, + pageNumber, + fields, + }: { + siteId: string; + filter?: string; + sort?: string; + pageSize?: number; + pageNumber?: number; + fields?: string; + }): Promise<{ pagination: Pagination; projects: Project[] }> => { + const response = await this._apiClient.queryProjects({ + params: { siteId }, + queries: { filter, sort, pageSize, pageNumber, fields }, + ...this.jsonHeaders(), + }); + return { + pagination: response.pagination, + projects: response.projects.project ?? [], + }; + }; + + /** + * Creates a project on the specified site. + * + * Required scopes: `tableau:content:update` (or site-specific project create capability) + */ + createProject = async ({ siteId, body }: { siteId: string; body: unknown }): Promise => + await this._apiClient.createProject({ + params: { siteId }, + body, + ...this.jsonWriteHeaders(), + }); + + /** + * Updates the specified project. + * + * Required scopes: `tableau:content:update` + */ + updateProject = async ({ + siteId, + projectId, + body, + }: { + siteId: string; + projectId: string; + body: unknown; + }): Promise => + await this._apiClient.updateProject({ + params: { siteId, projectId }, + body, + ...this.jsonWriteHeaders(), + }); + + /** + * Deletes the specified project. + * + * Required scopes: `tableau:content:delete` + */ + deleteProject = async ({ + siteId, + projectId, + }: { + siteId: string; + projectId: string; + }): Promise => + await this._apiClient.deleteProject({ + params: { siteId, projectId }, + ...this.jsonHeaders(), + }); +} diff --git a/src/tools/admin/content/contentProjects.ts b/src/tools/admin/content/contentProjects.ts new file mode 100644 index 000000000..4e9cc19b5 --- /dev/null +++ b/src/tools/admin/content/contentProjects.ts @@ -0,0 +1,123 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { JwtScopes, useRestApi } from '../../../restApiInstance.js'; +import { Server } from '../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = [ + 'query-projects', + 'create-project', + 'update-project', + 'delete-project', +] as const; + +type ContentProjectsOperation = (typeof operations)[number]; + +const jwtScopesByOperation: Record = { + 'query-projects': ['tableau:content:read'], + 'create-project': ['tableau:content:update'], + 'update-project': ['tableau:content:update'], + 'delete-project': ['tableau:content:delete'], +}; + +const paramsSchema = { + operation: z.enum(operations), + projectId: z.string().optional(), + filter: z.string().optional(), + sort: z.string().optional(), + pageSize: z.number().gt(0).optional(), + pageNumber: z.number().gt(0).optional(), + fields: z.string().optional(), + body: z.unknown().optional(), +}; + +const filteringDoc = + 'https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_filtering_and_sorting.htm'; + +export const getContentProjectsTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'content-projects', + description: `Tableau site projects: list with optional filter/sort/paging, create, update, or delete. Pass filter/sort using Tableau REST syntax (see ${filteringDoc}). Examples: updatedAt:gte:2024-01-01T00:00:00Z, name:eq:Sales.`, + paramsSchema, + annotations: { + title: 'Content — Projects', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => + await invokeOperation(restApi.projectsMethods, restApi.siteId, args), + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +async function invokeOperation( + projectsMethods: { + queryProjects: (p: { + siteId: string; + filter?: string; + sort?: string; + pageSize?: number; + pageNumber?: number; + fields?: string; + }) => Promise; + createProject: (p: { siteId: string; body: unknown }) => Promise; + updateProject: (p: { siteId: string; projectId: string; body: unknown }) => Promise; + deleteProject: (p: { siteId: string; projectId: string }) => Promise; + }, + siteId: string, + args: z.objectOutputType, +): Promise { + switch (args.operation) { + case 'query-projects': + return await projectsMethods.queryProjects({ + siteId, + filter: args.filter, + sort: args.sort, + pageSize: args.pageSize, + pageNumber: args.pageNumber, + fields: args.fields, + }); + case 'create-project': + return await projectsMethods.createProject({ + siteId, + body: required(args.body, 'body'), + }); + case 'update-project': + return await projectsMethods.updateProject({ + siteId, + projectId: required(args.projectId, 'projectId'), + body: required(args.body, 'body'), + }); + case 'delete-project': + return await projectsMethods.deleteProject({ + siteId, + projectId: required(args.projectId, 'projectId'), + }); + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null) { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/content/contentViews.ts b/src/tools/admin/content/contentViews.ts new file mode 100644 index 000000000..9d38286e4 --- /dev/null +++ b/src/tools/admin/content/contentViews.ts @@ -0,0 +1,213 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { JwtScopes, useRestApi } from '../../../restApiInstance.js'; +import { Server } from '../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = [ + 'get-view', + 'query-views-for-site', + 'query-views-for-workbook', + 'query-view-data', + 'query-view-image', + 'query-view-pdf', + 'download-view-crosstab-excel', +] as const; + +type ContentViewsOperation = (typeof operations)[number]; + +const jwtScopesByOperation: Record = { + 'get-view': ['tableau:content:read'], + 'query-views-for-site': ['tableau:content:read'], + 'query-views-for-workbook': ['tableau:content:read'], + 'query-view-data': ['tableau:views:download'], + 'query-view-image': ['tableau:views:download'], + 'query-view-pdf': ['tableau:views:download'], + 'download-view-crosstab-excel': ['tableau:views:download'], +}; + +const paramsSchema = { + operation: z.enum(operations), + viewId: z.string().optional(), + workbookId: z.string().optional(), + includeUsageStatistics: z.boolean().optional(), + filter: z.string().optional(), + sort: z.string().optional(), + pageSize: z.number().gt(0).optional(), + pageNumber: z.number().gt(0).optional(), + fields: z.string().optional(), + vizWidth: z.number().optional(), + vizHeight: z.number().optional(), + resolution: z.literal('high').optional(), + /** Extra query parameters for PDF or crosstab exports (e.g. maxAge). */ + exportQuery: z.record(z.string()).optional(), +}; + +const filteringDoc = + 'https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_filtering_and_sorting.htm'; + +const MAX_BINARY_BASE64_BYTES = 2 * 1024 * 1024; + +export const getContentViewsTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'content-views', + description: `Tableau views: get, list for site or workbook (filter/sort/paging — ${filteringDoc}), CSV data, image, PDF, or Excel crosstab. "Get view by URL name" uses query-views-for-site with filter viewUrlName:eq:SheetName. PDF/crosstab responses are base64-encoded up to ${MAX_BINARY_BASE64_BYTES} bytes.`, + paramsSchema, + annotations: { + title: 'Content — Views', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => + await invokeOperation(restApi.viewsMethods, restApi.siteId, args), + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +function toExportQueries( + record: Record | undefined, +): Record | undefined { + if (!record || Object.keys(record).length === 0) { + return undefined; + } + return record; +} + +async function invokeOperation( + viewsMethods: { + getView: (p: { siteId: string; viewId: string }) => Promise; + queryViewsForSite: (p: { + siteId: string; + includeUsageStatistics?: boolean; + filter?: string; + sort?: string; + fields?: string; + pageSize?: number; + pageNumber?: number; + }) => Promise; + queryViewsForWorkbook: (p: { + siteId: string; + workbookId: string; + includeUsageStatistics?: boolean; + }) => Promise; + queryViewData: (p: { siteId: string; viewId: string }) => Promise; + queryViewImage: (p: { + viewId: string; + siteId: string; + width?: number; + height?: number; + resolution?: 'high'; + }) => Promise; + queryViewPdf: (p: { + siteId: string; + viewId: string; + queries?: Record; + }) => Promise; + downloadViewCrosstabExcel: (p: { + siteId: string; + viewId: string; + queries?: Record; + }) => Promise; + }, + siteId: string, + args: z.objectOutputType, +): Promise { + switch (args.operation) { + case 'get-view': + return await viewsMethods.getView({ + siteId, + viewId: required(args.viewId, 'viewId'), + }); + case 'query-views-for-site': + return await viewsMethods.queryViewsForSite({ + siteId, + includeUsageStatistics: args.includeUsageStatistics, + filter: args.filter, + sort: args.sort, + fields: args.fields, + pageSize: args.pageSize, + pageNumber: args.pageNumber, + }); + case 'query-views-for-workbook': + return await viewsMethods.queryViewsForWorkbook({ + siteId, + workbookId: required(args.workbookId, 'workbookId'), + includeUsageStatistics: args.includeUsageStatistics, + }); + case 'query-view-data': + return await viewsMethods.queryViewData({ + siteId, + viewId: required(args.viewId, 'viewId'), + }); + case 'query-view-image': + return await viewsMethods.queryViewImage({ + siteId, + viewId: required(args.viewId, 'viewId'), + width: args.vizWidth, + height: args.vizHeight, + resolution: args.resolution, + }); + case 'query-view-pdf': { + const buf = await viewsMethods.queryViewPdf({ + siteId, + viewId: required(args.viewId, 'viewId'), + queries: toExportQueries(args.exportQuery), + }); + if (buf.byteLength > MAX_BINARY_BASE64_BYTES) { + throw new Error( + `PDF is ${buf.byteLength} bytes; max ${MAX_BINARY_BASE64_BYTES} for base64 MCP response.`, + ); + } + return { + encoding: 'base64', + contentType: 'application/pdf', + byteLength: buf.byteLength, + data: Buffer.from(buf).toString('base64'), + }; + } + case 'download-view-crosstab-excel': { + const buf = await viewsMethods.downloadViewCrosstabExcel({ + siteId, + viewId: required(args.viewId, 'viewId'), + queries: toExportQueries(args.exportQuery), + }); + if (buf.byteLength > MAX_BINARY_BASE64_BYTES) { + throw new Error( + `Crosstab export is ${buf.byteLength} bytes; max ${MAX_BINARY_BASE64_BYTES} for base64 MCP response.`, + ); + } + return { + encoding: 'base64', + contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + byteLength: buf.byteLength, + data: Buffer.from(buf).toString('base64'), + }; + } + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null) { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/content/contentWorkbooks.ts b/src/tools/admin/content/contentWorkbooks.ts new file mode 100644 index 000000000..da5a2fe44 --- /dev/null +++ b/src/tools/admin/content/contentWorkbooks.ts @@ -0,0 +1,179 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { JwtScopes, useRestApi } from '../../../restApiInstance.js'; +import { Server } from '../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = [ + 'get-workbook', + 'get-workbook-raw', + 'query-workbooks-for-site', + 'query-workbooks-for-user', + 'update-workbook', + 'delete-workbook', + 'download-workbook', +] as const; + +type ContentWorkbooksOperation = (typeof operations)[number]; + +const jwtScopesByOperation: Record = { + 'get-workbook': ['tableau:content:read'], + 'get-workbook-raw': ['tableau:content:read'], + 'query-workbooks-for-site': ['tableau:content:read'], + 'query-workbooks-for-user': ['tableau:content:read'], + 'update-workbook': ['tableau:content:update'], + 'delete-workbook': ['tableau:content:delete'], + 'download-workbook': ['tableau:views:download'], +}; + +const paramsSchema = { + operation: z.enum(operations), + workbookId: z.string().optional(), + userId: z.string().optional(), + /** For query-workbooks-for-user: "true" (owned only) or "false" (readable); Tableau REST `ownedBy`. */ + ownedBy: z.enum(['true', 'false']).optional(), + filter: z.string().optional(), + sort: z.string().optional(), + pageSize: z.number().gt(0).optional(), + pageNumber: z.number().gt(0).optional(), + fields: z.string().optional(), + body: z.unknown().optional(), +}; + +const filteringDoc = + 'https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_filtering_and_sorting.htm'; + +export const getContentWorkbooksTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'content-workbooks', + description: `Tableau workbooks on the site: get, list with filter/sort/paging, list for a user (resolve email to LUID with admin-users first), update, delete, or download .twbx. Filter/sort syntax: ${filteringDoc}. Examples: projectName:eq:Samples, ownerEmail:eq:user@example.com (if supported on your server version). After you resolve a workbook (e.g. by name filter), use content-permissions with list-granular-permissions, granularKind workbook, and resourceId = workbook id (LUID)—not the display name—for ACL rows.`, + paramsSchema, + annotations: { + title: 'Content — Workbooks', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => + await invokeOperation(restApi.workbooksMethods, restApi.siteId, args), + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +const MAX_DOWNLOAD_BASE64_BYTES = 2 * 1024 * 1024; + +async function invokeOperation( + workbooksMethods: { + getWorkbook: (p: { siteId: string; workbookId: string }) => Promise; + getWorkbookRaw: (p: { siteId: string; workbookId: string }) => Promise; + queryWorkbooksForSite: (p: { + siteId: string; + filter?: string; + sort?: string; + fields?: string; + pageSize?: number; + pageNumber?: number; + }) => Promise; + queryWorkbooksForUser: (p: { + siteId: string; + userId: string; + ownedBy?: 'true' | 'false'; + filter?: string; + sort?: string; + fields?: string; + pageSize?: number; + pageNumber?: number; + }) => Promise; + updateWorkbook: (p: { siteId: string; workbookId: string; body: unknown }) => Promise; + deleteWorkbook: (p: { siteId: string; workbookId: string }) => Promise; + downloadWorkbookContent: (p: { siteId: string; workbookId: string }) => Promise; + }, + siteId: string, + args: z.objectOutputType, +): Promise { + switch (args.operation) { + case 'get-workbook': + return await workbooksMethods.getWorkbook({ + siteId, + workbookId: required(args.workbookId, 'workbookId'), + }); + case 'get-workbook-raw': + return await workbooksMethods.getWorkbookRaw({ + siteId, + workbookId: required(args.workbookId, 'workbookId'), + }); + case 'query-workbooks-for-site': + return await workbooksMethods.queryWorkbooksForSite({ + siteId, + filter: args.filter, + sort: args.sort, + fields: args.fields, + pageSize: args.pageSize, + pageNumber: args.pageNumber, + }); + case 'query-workbooks-for-user': + return await workbooksMethods.queryWorkbooksForUser({ + siteId, + userId: required(args.userId, 'userId'), + ownedBy: args.ownedBy, + filter: args.filter, + sort: args.sort, + fields: args.fields, + pageSize: args.pageSize, + pageNumber: args.pageNumber, + }); + case 'update-workbook': + return await workbooksMethods.updateWorkbook({ + siteId, + workbookId: required(args.workbookId, 'workbookId'), + body: required(args.body, 'body'), + }); + case 'delete-workbook': + return await workbooksMethods.deleteWorkbook({ + siteId, + workbookId: required(args.workbookId, 'workbookId'), + }); + case 'download-workbook': { + const buf = await workbooksMethods.downloadWorkbookContent({ + siteId, + workbookId: required(args.workbookId, 'workbookId'), + }); + if (buf.byteLength > MAX_DOWNLOAD_BASE64_BYTES) { + throw new Error( + `Workbook download is ${buf.byteLength} bytes; max ${MAX_DOWNLOAD_BASE64_BYTES} for base64 MCP response.`, + ); + } + return { + encoding: 'base64', + contentType: 'application/octet-stream', + byteLength: buf.byteLength, + data: Buffer.from(buf).toString('base64'), + }; + } + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null) { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/groups/adminGroups.ts b/src/tools/admin/groups/adminGroups.ts new file mode 100644 index 000000000..d9dcf1957 --- /dev/null +++ b/src/tools/admin/groups/adminGroups.ts @@ -0,0 +1,236 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { useRestApi } from '../../../restApiInstance.js'; +import { Server } from '../../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = [ + 'add-group-to-group-set', + 'add-user-to-group', + 'create-group', + 'create-group-set', + 'delete-group', + 'delete-group-set', + 'get-group-set', + 'get-users-in-group', + 'list-group-sets', + 'query-groups', + 'remove-group-from-group-set', + 'remove-user-from-group', + 'bulk-remove-users-from-group', + 'update-group', + 'update-group-set', +] as const; + +type AdminGroupsOperation = (typeof operations)[number]; + +const jwtScopesByOperation: Record> = { + 'add-group-to-group-set': ['tableau:groupsets:update'], + 'add-user-to-group': ['tableau:groups:update'], + 'create-group': ['tableau:groups:create'], + 'create-group-set': ['tableau:groupsets:create'], + 'delete-group': ['tableau:groups:delete'], + 'delete-group-set': ['tableau:groupsets:delete'], + 'get-group-set': ['tableau:groupsets:read'], + 'get-users-in-group': ['tableau:groups:read'], + 'list-group-sets': ['tableau:groupsets:read'], + 'query-groups': ['tableau:groups:read'], + 'remove-group-from-group-set': ['tableau:groupsets:delete'], + 'remove-user-from-group': ['tableau:groups:update'], + 'bulk-remove-users-from-group': ['tableau:groups:update'], + 'update-group': ['tableau:groups:update'], + 'update-group-set': ['tableau:groupsets:update'], +}; + +const paramsSchema = { + operation: z.enum(operations), + siteId: z.string().optional(), + groupId: z.string().optional(), + groupSetId: z.string().optional(), + pageSize: z.number().gt(0).optional(), + pageNumber: z.number().gt(0).optional(), + filter: z.string().optional(), + sort: z.string().optional(), + asJob: z.boolean().optional(), + userId: z.string().optional(), + body: z.any().optional(), +}; + +export const getAdminGroupsTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'admin-groups', + description: + 'Administrative Tableau groups tool exposing non-SCIM group and group-set methods. Use this tool to create, update, delete, query, and manage group membership.', + paramsSchema, + annotations: { + title: 'Admin Groups', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => { + // Always use site LUID from authenticated context. + return await invokeOperation(restApi, restApi.siteId, args); + }, + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +async function invokeOperation( + restApi: { + adminMethods: { + addGroupToGroupSet: (siteId: string, groupSetId: string, groupId: string) => Promise; + addUserToGroup: (siteId: string, groupId: string, body: unknown) => Promise; + createGroup: ( + siteId: string, + body: unknown, + queries?: { asJob?: boolean }, + ) => Promise; + createGroupSet: (siteId: string, body: unknown) => Promise; + deleteGroup: (siteId: string, groupId: string) => Promise; + deleteGroupSet: (siteId: string, groupSetId: string) => Promise; + getGroupSet: (siteId: string, groupSetId: string) => Promise; + getUsersInGroup: ( + siteId: string, + groupId: string, + queries?: { pageSize?: number; pageNumber?: number }, + ) => Promise; + listGroupSets: ( + siteId: string, + queries?: { pageSize?: number; pageNumber?: number; filter?: string; sort?: string }, + ) => Promise; + queryGroups: ( + siteId: string, + queries?: { pageSize?: number; pageNumber?: number; filter?: string; sort?: string }, + ) => Promise; + removeGroupFromGroupSet: ( + siteId: string, + groupSetId: string, + groupId: string, + ) => Promise; + removeUserFromGroup: (siteId: string, groupId: string, userId: string) => Promise; + bulkRemoveUsersFromGroup: ( + siteId: string, + groupId: string, + body: unknown, + ) => Promise; + updateGroup: ( + siteId: string, + groupId: string, + body: unknown, + queries?: { asJob?: boolean }, + ) => Promise; + updateGroupSet: (siteId: string, groupSetId: string, body: unknown) => Promise; + }; + }, + siteId: string, + args: z.objectOutputType, +): Promise { + switch (args.operation) { + case 'add-group-to-group-set': + return await restApi.adminMethods.addGroupToGroupSet( + siteId, + required(args.groupSetId, 'groupSetId'), + required(args.groupId, 'groupId'), + ); + case 'add-user-to-group': + return await restApi.adminMethods.addUserToGroup( + siteId, + required(args.groupId, 'groupId'), + required(args.body, 'body'), + ); + case 'create-group': + return await restApi.adminMethods.createGroup(siteId, required(args.body, 'body'), { + asJob: args.asJob, + }); + case 'create-group-set': + return await restApi.adminMethods.createGroupSet(siteId, required(args.body, 'body')); + case 'delete-group': + return await restApi.adminMethods.deleteGroup(siteId, required(args.groupId, 'groupId')); + case 'delete-group-set': + return await restApi.adminMethods.deleteGroupSet( + siteId, + required(args.groupSetId, 'groupSetId'), + ); + case 'get-group-set': + return await restApi.adminMethods.getGroupSet( + siteId, + required(args.groupSetId, 'groupSetId'), + ); + case 'get-users-in-group': + return await restApi.adminMethods.getUsersInGroup(siteId, required(args.groupId, 'groupId'), { + pageSize: args.pageSize, + pageNumber: args.pageNumber, + }); + case 'list-group-sets': + return await restApi.adminMethods.listGroupSets(siteId, { + pageSize: args.pageSize, + pageNumber: args.pageNumber, + filter: args.filter, + sort: args.sort, + }); + case 'query-groups': + return await restApi.adminMethods.queryGroups(siteId, { + pageSize: args.pageSize, + pageNumber: args.pageNumber, + filter: args.filter, + sort: args.sort, + }); + case 'remove-group-from-group-set': + return await restApi.adminMethods.removeGroupFromGroupSet( + siteId, + required(args.groupSetId, 'groupSetId'), + required(args.groupId, 'groupId'), + ); + case 'remove-user-from-group': + return await restApi.adminMethods.removeUserFromGroup( + siteId, + required(args.groupId, 'groupId'), + required(args.userId, 'userId'), + ); + case 'bulk-remove-users-from-group': + return await restApi.adminMethods.bulkRemoveUsersFromGroup( + siteId, + required(args.groupId, 'groupId'), + required(args.body, 'body'), + ); + case 'update-group': + return await restApi.adminMethods.updateGroup( + siteId, + required(args.groupId, 'groupId'), + required(args.body, 'body'), + { asJob: args.asJob }, + ); + case 'update-group-set': + return await restApi.adminMethods.updateGroupSet( + siteId, + required(args.groupSetId, 'groupSetId'), + required(args.body, 'body'), + ); + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null) { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/jobs/siteJobs.ts b/src/tools/admin/jobs/siteJobs.ts new file mode 100644 index 000000000..e5100f224 --- /dev/null +++ b/src/tools/admin/jobs/siteJobs.ts @@ -0,0 +1,87 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { useRestApi } from '../../../restApiInstance.js'; +import { RestApi } from '../../sdks/tableau/restApi.js'; +import { Server } from '../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = ['query-jobs', 'query-job', 'cancel-job'] as const; + +type SiteJobsOperation = (typeof operations)[number]; + +const jwtScopesByOperation: Record> = { + 'query-jobs': ['tableau:jobs:read'], + 'query-job': ['tableau:jobs:read'], + 'cancel-job': ['tableau:jobs:update'], +}; + +const paramsSchema = { + operation: z.enum(operations), + siteId: z.string().optional(), + jobId: z.string().optional(), + filter: z.string().optional(), + pageSize: z.number().gt(0).optional(), + pageNumber: z.number().gt(0).optional(), +}; + +export const getSiteJobsTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'site-jobs', + description: + 'Query active background jobs on the site, fetch a single job, or cancel a job (site/server administrators). Cancel uses HTTP PUT per Tableau REST API.', + paramsSchema, + annotations: { + title: 'Site jobs', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => { + return await invokeOperation(restApi, restApi.siteId, args); + }, + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +type Args = z.infer>; + +async function invokeOperation(restApi: RestApi, siteId: string, args: Args): Promise { + const jm = restApi.jobsMethods; + switch (args.operation) { + case 'query-jobs': + return await jm.queryJobs(siteId, { + filter: args.filter, + pageSize: args.pageSize, + pageNumber: args.pageNumber, + }); + case 'query-job': + return await jm.queryJob(siteId, required(args.jobId, 'jobId')); + case 'cancel-job': + return await jm.cancelJob(siteId, required(args.jobId, 'jobId')); + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null || value === '') { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/operations/tableauOperations.ts b/src/tools/admin/operations/tableauOperations.ts new file mode 100644 index 000000000..7e4528878 --- /dev/null +++ b/src/tools/admin/operations/tableauOperations.ts @@ -0,0 +1,636 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { useRestApi } from '../../../restApiInstance.js'; +import { uploadTwbxToS3 } from '../../../utils/uploadWorkbookToS3.js'; +import type { Config } from '../../config.js'; +import { RestApi } from '../../sdks/tableau/restApi.js'; +import { Server } from '../../server.js'; +import { Tool } from '../../tool.js'; +import { + effectiveReadForUser, + extractBackgroundJobs, + extractGranteeRules, + extractWorkbookLuidsFromJob, + extractWorkbookNode, + extractWorkbookOwnerUserId, + groupOverlappingJobs, + isContentPermissionsUnlocked, + parseIsoUtc, + workbookLineageQuery, +} from './tableauOperationsHelpers.js'; + +const operations = [ + 'get-background-job-conflicts', + 'get-job-performance-stats', + 'kill-job-by-priority', + 'get-effective-permissions', + 'trace-access-reason', + 'list-content-overrides', + 'get-stale-content-report', + 'get-workbook-lineage-impact', + 'archive-workbook', +] as const; + +type TableauOperationsOp = (typeof operations)[number]; + +const jwtScopesByOperation: Record> = { + 'get-background-job-conflicts': ['tableau:jobs:read', 'tableau:content:read'], + 'get-job-performance-stats': ['tableau:jobs:read'], + 'kill-job-by-priority': ['tableau:jobs:read', 'tableau:jobs:update'], + 'get-effective-permissions': [ + 'tableau:permissions:read', + 'tableau:users:read', + 'tableau:groups:read', + ], + 'trace-access-reason': ['tableau:permissions:read', 'tableau:content:read'], + 'list-content-overrides': ['tableau:content:read'], + 'get-stale-content-report': ['tableau:content:read'], + 'get-workbook-lineage-impact': ['tableau:content:read'], + 'archive-workbook': ['tableau:content:read', 'tableau:views:download'], +}; + +const paramsSchema = { + operation: z.enum(operations), + siteId: z.string().optional(), + workbookId: z.string().optional(), + userId: z.string().optional(), + projectFilter: z + .string() + .optional() + .describe('REST filter for workbooks, e.g. projectId:eq:luid or projectName:eq:Name'), + staleDays: z.number().gt(0).optional(), + runningThresholdMinutes: z.number().gt(0).optional(), + minPriority: z.number().optional(), + overlapWindowMs: z.number().gt(0).optional(), + dryRun: z.boolean().optional(), + maxCancels: z.number().gt(0).optional(), + maxBase64Bytes: z.number().gt(0).optional(), +}; + +export const getTableauOperationsTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'tableau-operations', + description: + 'Higher-level Tableau Cloud operations (no Postgres): job overlap heuristics with optional Metadata enrichment for workbook→database context; live-only job duration flags (no historical μ/σ); bulk cancel with dryRun; get-effective-permissions gives effective Read for one userId + one workbookId (heuristic), not a site-wide list of viewers—for workbook ACL grantees use content-permissions list-granular-permissions; access trace; project permission overrides; stale workbooks; lineage/impact GraphQL; archive as S3 upload when TABLEAU_ARCHIVE_* env is set, else base64 for small .twbx. Defaults: TABLEAU_OPS_RUNNING_THRESHOLD_MINUTES, TABLEAU_OPS_STALE_DAYS, TABLEAU_OPS_OVERLAP_WINDOW_MS.', + paramsSchema, + annotations: { + title: 'Tableau operations', + readOnlyHint: false, + openWorldHint: true, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + const { config } = extra; + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => { + return await runOperation(restApi, restApi.siteId, args, config); + }, + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +type Args = z.infer>; + +async function runOperation( + restApi: RestApi, + siteId: string, + args: Args, + config: Config, +): Promise { + switch (args.operation) { + case 'get-background-job-conflicts': + return await opJobConflicts(restApi, siteId, args, config); + case 'get-job-performance-stats': + return await opJobPerf(restApi, siteId, args, config); + case 'kill-job-by-priority': + return await opKillJobs(restApi, siteId, args); + case 'get-effective-permissions': + return await opEffectivePerms(restApi, siteId, args); + case 'trace-access-reason': + return await opTraceAccess(restApi, siteId, args); + case 'list-content-overrides': + return await opListOverrides(restApi, siteId, args); + case 'get-stale-content-report': + return await opStaleReport(restApi, siteId, args, config); + case 'get-workbook-lineage-impact': + return await opLineage(restApi, args); + case 'archive-workbook': + return await opArchive(restApi, siteId, args, config); + } +} + +async function opJobConflicts( + restApi: RestApi, + siteId: string, + args: Args, + config: Config, +): Promise { + const data = await restApi.jobsMethods.queryJobs(siteId, { pageSize: 1000, pageNumber: 1 }); + const jobs = extractBackgroundJobs(data); + const windowMs = args.overlapWindowMs ?? config.tableauOpsOverlapWindowMs; + const conflicts = groupOverlappingJobs(jobs, windowMs); + + const metadataByWorkbook: Record = {}; + if (config.tableauOpsEnrichJobConflictsWithMetadata) { + const wbSet = new Set(); + for (const j of jobs) { + for (const id of extractWorkbookLuidsFromJob(j)) { + wbSet.add(id); + } + } + const maxMeta = 25; + for (const wid of [...wbSet].slice(0, maxMeta)) { + try { + const q = ` +query { + workbooks(filter: {luid: "${wid}"}) { + luid + name + upstreamDatabases { name hostName connectionType } + upstreamDatasources { luid name } + } +} +`.trim(); + metadataByWorkbook[wid] = await restApi.metadataMethods.graphql(q); + } catch (e) { + metadataByWorkbook[wid] = { error: String(e) }; + } + } + } + + return { + note: 'Heuristic overlap by jobType/title and time window; optional Metadata shows upstream DBs/datasources per workbook LUID found in job payloads.', + totalJobs: jobs.length, + overlapWindowMs: windowMs, + conflictGroups: conflicts, + metadataEnrichment: config.tableauOpsEnrichJobConflictsWithMetadata + ? metadataByWorkbook + : undefined, + }; +} + +async function opJobPerf( + restApi: RestApi, + siteId: string, + args: Args, + config: Config, +): Promise { + const data = await restApi.jobsMethods.queryJobs(siteId, { pageSize: 1000, pageNumber: 1 }); + const jobs = extractBackgroundJobs(data); + const thresholdMin = args.runningThresholdMinutes ?? config.tableauOpsRunningThresholdMinutes; + const thresholdMs = thresholdMin * 60_000; + const now = Date.now(); + const flagged: Record[] = []; + for (const j of jobs) { + const started = parseIsoUtc(j.startedAt ?? j.started_at ?? j.createdAt); + const ended = parseIsoUtc(j.endedAt ?? j.ended_at); + if (started == null) { + continue; + } + if (ended == null) { + if (now - started > thresholdMs) { + flagged.push({ + id: j.id, + reason: 'running_exceeds_threshold', + runningMs: now - started, + }); + } + continue; + } + const dur = ended - started; + if (dur > thresholdMs) { + flagged.push({ + id: j.id, + reason: 'completed_duration_exceeds_threshold', + durationMs: dur, + }); + } + } + return { + note: 'Tableau Cloud has no public historical job stats; flags use live jobs and a fixed duration threshold only.', + thresholdMinutes: thresholdMin, + flaggedJobs: flagged, + }; +} + +async function opKillJobs(restApi: RestApi, siteId: string, args: Args): Promise { + const data = await restApi.jobsMethods.queryJobs(siteId, { pageSize: 1000, pageNumber: 1 }); + const jobs = extractBackgroundJobs(data); + // Tableau: lower numeric priority = more important; large values are lower-priority work. + const minP = args.minPriority ?? 100; + const maxCancels = args.maxCancels ?? 25; + const candidates = jobs.filter((j) => { + const p = Number(j.priority ?? j['priority'] ?? 0); + return p >= minP && !parseIsoUtc(j.endedAt ?? j.ended_at); + }); + const ids = candidates + .map((j) => String(j.id ?? '')) + .filter(Boolean) + .slice(0, maxCancels); + if (args.dryRun !== false) { + return { + dryRun: true, + message: 'Pass dryRun:false to cancel jobs (still limited by maxCancels).', + minPriority: minP, + candidateJobIds: ids, + }; + } + const results: Array<{ jobId: string; ok: boolean; error?: string }> = []; + for (const jobId of ids) { + try { + await restApi.jobsMethods.cancelJob(siteId, jobId); + results.push({ jobId, ok: true }); + } catch (e) { + results.push({ jobId, ok: false, error: String(e) }); + } + } + return { cancelled: results }; +} + +async function opEffectivePerms(restApi: RestApi, siteId: string, args: Args): Promise { + const workbookId = required(args.workbookId, 'workbookId'); + const permData = await restApi.permissionsMethods.listGranularPermissions( + siteId, + 'workbook', + workbookId, + ); + const rawWb = await restApi.workbooksMethods.getWorkbookRaw({ siteId, workbookId }); + const workbookOwnerUserId = extractWorkbookOwnerUserId(extractWorkbookNode(rawWb)); + + const { users: userRules, groups: groupRules } = extractGranteeRules(permData); + + const groupMemberMap = new Map>(); + for (const gid of groupRules.keys()) { + const members = await fetchAllGroupUserIds(restApi, siteId, gid); + groupMemberMap.set(gid, members); + } + + const usersOut: Array<{ + userId: string; + siteRole?: string; + effectiveRead: string; + reason: string; + }> = []; + + for await (const u of iterateSiteUsers(restApi, siteId)) { + const uid = u.id; + const direct = userRules.get(uid); + const groupCapsList: { name: string; mode: string }[][] = []; + for (const [gid, caps] of groupRules) { + if (groupMemberMap.get(gid)?.has(uid)) { + groupCapsList.push(caps); + } + } + const { effective, reason } = effectiveReadForUser( + u.siteRole, + uid, + workbookOwnerUserId, + direct, + groupCapsList, + ); + usersOut.push({ + userId: uid, + siteRole: u.siteRole, + effectiveRead: effective, + reason, + }); + } + + return { + note: 'Read capability only; order: site/server admin → workbook owner → user deny/allow → group deny/allow. Verify in Tableau UI for critical decisions.', + workbookId, + workbookOwnerUserId: workbookOwnerUserId ?? null, + users: usersOut, + }; +} + +async function opTraceAccess(restApi: RestApi, siteId: string, args: Args): Promise { + const workbookId = required(args.workbookId, 'workbookId'); + const userId = required(args.userId, 'userId'); + const permData = await restApi.permissionsMethods.listGranularPermissions( + siteId, + 'workbook', + workbookId, + ); + + const queryString = ` +query { + workbooks(filter: {luid: "${workbookId}"}) { + name + projectName + owner { name email } + } +} +`.trim(); + const meta = await restApi.metadataMethods.graphql(queryString); + const { users: userRules, groups: groupRules } = extractGranteeRules(permData); + const userDirect = userRules.get(userId); + const groupIds: string[] = []; + for (const [gid] of groupRules) { + const members = await fetchAllGroupUserIds(restApi, siteId, gid); + if (members.has(userId)) { + groupIds.push(gid); + } + } + + const explanationSteps = [ + { + step: 1, + detail: 'Resolved workbook and project context from Metadata API (name, owner email).', + }, + { + step: 2, + detail: + userDirect && userDirect.length > 0 + ? 'User has direct capability rules on this workbook.' + : 'No direct user capability rows on this workbook.', + }, + { + step: 3, + detail: + groupIds.length > 0 + ? `User is a member of ${groupIds.length} group(s) with rules on this workbook.` + : 'User is not in any groups that have rules on this workbook (among groups listed on the ACL).', + }, + ]; + + return { + userId, + workbookId, + metadata: meta, + directUserCapabilities: userDirect ?? null, + groupIdsContributing: groupIds, + explanationSteps, + rawPermissionsFragment: permData, + }; +} + +async function opListOverrides(restApi: RestApi, siteId: string, args: Args): Promise { + const filter = required(args.projectFilter, 'projectFilter'); + const overrides: Array<{ workbookId: string; name?: string }> = []; + let page = 1; + const pageSize = 100; + for (;;) { + const batch = await restApi.workbooksMethods.queryWorkbooksForSite({ + siteId, + filter, + pageSize, + pageNumber: page, + }); + const wbs = batch.workbooks; + if (wbs.length === 0) { + break; + } + for (const wb of wbs) { + const raw = await restApi.workbooksMethods.getWorkbookRaw({ siteId, workbookId: wb.id }); + const node = extractWorkbookNode(raw); + if (isContentPermissionsUnlocked(node)) { + overrides.push({ workbookId: wb.id, name: wb.name }); + } + } + if (wbs.length < pageSize) { + break; + } + page += 1; + if (page > 50) { + break; + } + } + return { projectFilter: filter, workbooksWithUnlockedContentPermissions: overrides }; +} + +async function opStaleReport( + restApi: RestApi, + siteId: string, + args: Args, + config: Config, +): Promise { + const staleDays = args.staleDays ?? config.tableauOpsStaleDays; + const cutoff = Date.now() - staleDays * 86400000; + const stale: Array<{ workbookId: string; name?: string; updatedAt?: string }> = []; + let page = 1; + const pageSize = 100; + for (;;) { + const batch = await restApi.workbooksMethods.queryWorkbooksForSite({ + siteId, + pageSize, + pageNumber: page, + }); + const wbs = batch.workbooks; + if (wbs.length === 0) { + break; + } + for (const wb of wbs) { + const raw = await restApi.workbooksMethods.getWorkbookRaw({ siteId, workbookId: wb.id }); + const node = extractWorkbookNode(raw) as Record | null; + const updatedAt = node + ? String(node.updatedAt ?? node.updated_at ?? node.createdAt ?? '') + : ''; + const t = parseIsoUtc(updatedAt); + if (t != null && t < cutoff) { + stale.push({ workbookId: wb.id, name: wb.name, updatedAt: updatedAt || undefined }); + } + } + if (wbs.length < pageSize) { + break; + } + page += 1; + if (page > 30) { + break; + } + } + return { + note: 'Uses workbook updatedAt/createdAt from REST JSON when present; validate fields on your site.', + staleDays, + staleWorkbooks: stale, + }; +} + +async function opLineage(restApi: RestApi, args: Args): Promise { + const workbookId = required(args.workbookId, 'workbookId'); + const q = workbookLineageQuery().replace( + 'query GetWorkbookLineage($workbookId: String!)', + 'query GetWorkbookLineage', + ); + const query = q.replace( + 'workbooks(filter: {luid: $workbookId})', + `workbooks(filter: {luid: "${workbookId}"})`, + ); + return await restApi.metadataMethods.graphql(query); +} + +async function opArchive( + restApi: RestApi, + siteId: string, + args: Args, + config: Config, +): Promise { + const workbookId = required(args.workbookId, 'workbookId'); + const maxBytes = args.maxBase64Bytes ?? 524_288; + const buf = await restApi.workbooksMethods.downloadWorkbookContent({ siteId, workbookId }); + const bytes = buf.byteLength; + const buffer = Buffer.from(buf); + + if (config.tableauArchiveS3Bucket && config.tableauArchiveAwsRegion) { + const key = `${config.tableauArchiveS3Prefix}/${workbookId}-${Date.now()}.twbx`; + const { etag } = await uploadTwbxToS3({ + bucket: config.tableauArchiveS3Bucket, + key, + region: config.tableauArchiveAwsRegion, + body: buffer, + }); + return { + workbookId, + sizeBytes: bytes, + s3: { + bucket: config.tableauArchiveS3Bucket, + key, + etag, + }, + note: 'Uploaded via default AWS credential chain (e.g. AWS_ACCESS_KEY_ID / IAM role).', + }; + } + + if (bytes > maxBytes) { + return { + workbookId, + sizeBytes: bytes, + message: `Workbook exceeds maxBase64Bytes (${maxBytes}). Set TABLEAU_ARCHIVE_S3_BUCKET and TABLEAU_ARCHIVE_AWS_REGION to upload large .twbx files, or download via REST.`, + }; + } + const b64 = buffer.toString('base64'); + return { + workbookId, + sizeBytes: bytes, + contentBase64: b64, + note: '.twbx as base64; store securely. For large files configure TABLEAU_ARCHIVE_S3_* env vars.', + }; +} + +async function* iterateSiteUsers( + restApi: RestApi, + siteId: string, +): AsyncGenerator<{ id: string; siteRole?: string }> { + let page = 1; + const pageSize = 1000; + for (;;) { + const data = await restApi.adminMethods.getUsersOnSite(siteId, { + pageSize, + pageNumber: page, + }); + const users = extractUsersFromSitePayload(data); + if (users.length === 0) { + break; + } + for (const u of users) { + yield u; + } + if (users.length < pageSize) { + break; + } + page += 1; + if (page > 100) { + break; + } + } +} + +function extractUsersFromSitePayload(data: unknown): Array<{ id: string; siteRole?: string }> { + if (!data || typeof data !== 'object') { + return []; + } + const d = data as Record; + const usersRoot = d.users as Record | undefined; + const raw = usersRoot?.user ?? usersRoot?.['user']; + const list = Array.isArray(raw) ? raw : raw != null ? [raw] : []; + const out: Array<{ id: string; siteRole?: string }> = []; + for (const item of list) { + if (!item || typeof item !== 'object') { + continue; + } + const u = item as Record; + const id = String(u.id ?? u['@id'] ?? ''); + if (!id) { + continue; + } + const siteRole = String(u.siteRole ?? u.site_role ?? u['siteRole'] ?? ''); + out.push({ id, siteRole: siteRole || undefined }); + } + return out; +} + +async function fetchAllGroupUserIds( + restApi: RestApi, + siteId: string, + groupId: string, +): Promise> { + const ids = new Set(); + let page = 1; + const pageSize = 1000; + for (;;) { + const data = await restApi.adminMethods.getUsersInGroup(siteId, groupId, { + pageSize, + pageNumber: page, + }); + const users = extractUsersFromGroupPayload(data); + if (users.length === 0) { + break; + } + for (const id of users) { + ids.add(id); + } + if (users.length < pageSize) { + break; + } + page += 1; + if (page > 50) { + break; + } + } + return ids; +} + +function extractUsersFromGroupPayload(data: unknown): string[] { + if (!data || typeof data !== 'object') { + return []; + } + const d = data as Record; + const usersRoot = d.users as Record | undefined; + const raw = usersRoot?.user ?? usersRoot?.['user']; + const list = Array.isArray(raw) ? raw : raw != null ? [raw] : []; + const out: string[] = []; + for (const item of list) { + if (!item || typeof item !== 'object') { + continue; + } + const u = item as Record; + const id = String(u.id ?? u['@id'] ?? ''); + if (id) { + out.push(id); + } + } + return out; +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null || value === '') { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/operations/tableauOperationsHelpers.ts b/src/tools/admin/operations/tableauOperationsHelpers.ts new file mode 100644 index 000000000..769371727 --- /dev/null +++ b/src/tools/admin/operations/tableauOperationsHelpers.ts @@ -0,0 +1,322 @@ +/** Pull Tableau-style `backgroundJob` rows from Query Jobs JSON (shape varies by version). */ +export function extractBackgroundJobs(payload: unknown): Record[] { + if (!payload || typeof payload !== 'object') { + return []; + } + const p = payload as Record; + const jobsRoot = + (p.backgroundJobs as Record | undefined) ?? + (p.backgroundjobs as Record | undefined); + if (!jobsRoot) { + return []; + } + const raw = + jobsRoot.backgroundJob ?? + jobsRoot.backgroundjob ?? + jobsRoot['backgroundJob'] ?? + jobsRoot['backgroundjob']; + if (raw == null) { + return []; + } + return Array.isArray(raw) ? (raw as Record[]) : [raw as Record]; +} + +export function parseIsoUtc(s: unknown): number | null { + if (typeof s !== 'string') { + return null; + } + const t = Date.parse(s); + return Number.isNaN(t) ? null : t; +} + +export function groupOverlappingJobs( + jobs: Record[], + windowMs: number, +): Array<{ windowKey: string; jobIds: string[]; count: number }> { + type Enriched = { id: string; start: number; end: number | null; jobType: string; title: string }; + const enriched: Enriched[] = []; + for (const j of jobs) { + const id = String(j.id ?? j['@id'] ?? ''); + if (!id) { + continue; + } + const created = parseIsoUtc(j.createdAt ?? j.created_at); + const started = parseIsoUtc(j.startedAt ?? j.started_at) ?? created; + const ended = parseIsoUtc(j.endedAt ?? j.ended_at); + const jobType = String(j.jobType ?? j.job_type ?? j.type ?? 'unknown'); + const title = String(j.title ?? j['title'] ?? ''); + const start = started ?? Date.now(); + const end = ended ?? start + windowMs; + enriched.push({ + id, + start, + end, + jobType, + title, + }); + } + + const buckets = new Map(); + for (let i = 0; i < enriched.length; i++) { + for (let k = i + 1; k < enriched.length; k++) { + const a = enriched[i]; + const b = enriched[k]; + const overlap = + a.start <= b.end + windowMs && + b.start <= (a.end ?? a.start + windowMs) + windowMs && + (a.jobType === b.jobType || normalizeTitleKey(a.title) === normalizeTitleKey(b.title)); + if (overlap) { + const key = `${a.jobType}|${normalizeTitleKey(a.title) || normalizeTitleKey(b.title) || 'overlap'}`; + const list = buckets.get(key) ?? []; + if (!list.includes(a.id)) { + list.push(a.id); + } + if (!list.includes(b.id)) { + list.push(b.id); + } + buckets.set(key, list); + } + } + } + + return [...buckets.entries()].map(([windowKey, jobIds]) => ({ + windowKey, + jobIds, + count: jobIds.length, + })); +} + +function normalizeTitleKey(title: string): string { + const t = title.trim().toLowerCase(); + return t.length === 0 ? '' : t.slice(0, 80); +} + +/** Collect workbook LUIDs referenced inside a job payload (nested Tableau JSON). */ +export function extractWorkbookLuidsFromJob(job: Record): string[] { + const found = new Set(); + const visit = (node: unknown): void => { + if (!node || typeof node !== 'object') { + return; + } + const o = node as Record; + const wb = o.workbook ?? o.Workbook; + if (wb && typeof wb === 'object') { + const wid = (wb as Record).id ?? (wb as Record)['@id']; + if (typeof wid === 'string' && wid.length > 10) { + found.add(wid); + } + } + for (const v of Object.values(o)) { + if (Array.isArray(v)) { + for (const x of v) { + visit(x); + } + } else { + visit(v); + } + } + }; + visit(job); + return [...found]; +} + +const LINEAGE_QUERY = ` +query GetWorkbookLineage($workbookId: String!) { + workbooks(filter: {luid: $workbookId}) { + luid + name + vizportalUrlId + upstreamDatabases { + name + connectionType + hostName + } + upstreamDatasources { + luid + name + } + embeddedDatasources { + name + } + owner { + name + email + } + projectName + } +} +`.trim(); + +export function workbookLineageQuery(): string { + return LINEAGE_QUERY; +} + +type Cap = { name: string; mode: string }; + +function normalizeCaps(capsUnknown: unknown): Cap[] { + if (!capsUnknown || typeof capsUnknown !== 'object') { + return []; + } + const c = capsUnknown as Record; + const cap = c.capability ?? c['capability']; + if (cap == null) { + return []; + } + const arr = Array.isArray(cap) ? cap : [cap]; + const out: Cap[] = []; + for (const x of arr) { + if (!x || typeof x !== 'object') { + continue; + } + const o = x as Record; + const name = String(o.name ?? o['@name'] ?? ''); + const mode = String(o.mode ?? o['@mode'] ?? ''); + if (name && mode) { + out.push({ name, mode }); + } + } + return out; +} + +/** Flatten grantee + capabilities from permissions JSON (best-effort). */ +export function extractGranteeRules(permissionsPayload: unknown): { + users: Map; + groups: Map; +} { + const users = new Map(); + const groups = new Map(); + + const walk = (node: unknown): void => { + if (!node || typeof node !== 'object') { + return; + } + const n = node as Record; + if ('granteeCapabilities' in n) { + const gc = n.granteeCapabilities as unknown; + const list = Array.isArray(gc) ? gc : gc != null ? [gc] : []; + for (const item of list) { + if (!item || typeof item !== 'object') { + continue; + } + const it = item as Record; + const u = it.user as Record | undefined; + const g = it.group as Record | undefined; + const uid = u ? String(u.id ?? u['@id'] ?? '') : ''; + const gid = g ? String(g.id ?? g['@id'] ?? '') : ''; + const caps = normalizeCaps(it.capabilities); + if (uid) { + const prev = users.get(uid) ?? []; + users.set(uid, [...prev, ...caps]); + } + if (gid) { + const prev = groups.get(gid) ?? []; + groups.set(gid, [...prev, ...caps]); + } + } + } + for (const v of Object.values(n)) { + if (v && typeof v === 'object') { + walk(v); + } + } + }; + + walk(permissionsPayload); + return { users, groups }; +} + +export function effectiveReadForUser( + siteRole: string | undefined, + userId: string, + workbookOwnerUserId: string | undefined, + userCaps: Cap[] | undefined, + groupCapsList: Cap[][], +): { effective: 'Allow' | 'Deny' | 'None'; reason: string } { + const sr = (siteRole ?? '').toLowerCase(); + if (sr.includes('siteadministrator') || sr.includes('serveradministrator')) { + return { effective: 'Allow', reason: 'site_or_server_admin_role' }; + } + + if (workbookOwnerUserId && userId && userId.toLowerCase() === workbookOwnerUserId.toLowerCase()) { + return { effective: 'Allow', reason: 'workbook_owner' }; + } + + const readCap = (c: Cap): boolean => c.name === 'Read' || c.name.includes('Read'); + const userRead = userCaps?.filter(readCap) ?? []; + const userDeny = userRead.some((c) => c.mode === 'Deny'); + const userAllow = userRead.some((c) => c.mode === 'Allow'); + if (userDeny) { + return { effective: 'Deny', reason: 'explicit_user_deny_read' }; + } + if (userAllow) { + return { effective: 'Allow', reason: 'explicit_user_allow_read' }; + } + + let groupHasDeny = false; + let groupHasAllow = false; + for (const caps of groupCapsList) { + const gr = caps.filter(readCap); + if (gr.some((c) => c.mode === 'Deny')) { + groupHasDeny = true; + } + if (gr.some((c) => c.mode === 'Allow')) { + groupHasAllow = true; + } + } + if (groupHasDeny) { + return { effective: 'Deny', reason: 'group_deny_read' }; + } + if (groupHasAllow) { + return { effective: 'Allow', reason: 'group_allow_read' }; + } + + return { effective: 'None', reason: 'no_matching_rule_default_deny_for_read' }; +} + +/** Heuristic: content not locked to project (field names vary). */ +export function isContentPermissionsUnlocked(workbookNode: unknown): boolean { + if (!workbookNode || typeof workbookNode !== 'object') { + return false; + } + const w = workbookNode as Record; + const cp = w.contentPermissions ?? w.contentpermissions; + if (!cp || typeof cp !== 'object') { + return false; + } + const cpo = cp as Record; + const raw = cpo.contentPermission ?? cpo.contentpermission; + const list = Array.isArray(raw) ? raw : raw != null ? [raw] : []; + for (const item of list) { + if (!item || typeof item !== 'object') { + continue; + } + const it = item as Record; + const name = String(it.name ?? it['@name'] ?? '').toLowerCase(); + const val = String(it.value ?? it['@value'] ?? it['#text'] ?? '').toLowerCase(); + if (name.includes('locked') && (val === 'false' || val === '0')) { + return true; + } + } + return false; +} + +export function extractWorkbookOwnerUserId(workbookNode: unknown): string | undefined { + if (!workbookNode || typeof workbookNode !== 'object') { + return undefined; + } + const w = workbookNode as Record; + const owner = w.owner ?? w.Owner; + if (!owner || typeof owner !== 'object') { + return undefined; + } + const id = (owner as Record).id ?? (owner as Record)['@id']; + return typeof id === 'string' && id.length > 0 ? id : undefined; +} + +export function extractWorkbookNode(workbookResponse: unknown): unknown { + if (!workbookResponse || typeof workbookResponse !== 'object') { + return null; + } + const r = workbookResponse as Record; + return r.workbook ?? r['workbook'] ?? null; +} diff --git a/src/tools/admin/permissions/contentPermissions.ts b/src/tools/admin/permissions/contentPermissions.ts new file mode 100644 index 000000000..e889d2748 --- /dev/null +++ b/src/tools/admin/permissions/contentPermissions.ts @@ -0,0 +1,212 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { useRestApi } from '../../../restApiInstance.js'; +import type { + DefaultPermissionSegment, + GranteePathKind, + GranularPermissionKind, + ReplaceContentKind, + ReplaceProjectDefaultSegment, +} from '../../sdks/tableau/methods/permissionsMethods.js'; +import { RestApi } from '../../sdks/tableau/restApi.js'; +import { Server } from '../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = [ + 'list-granular-permissions', + 'add-granular-permissions', + 'delete-granular-permission', + 'list-default-permissions', + 'add-default-permissions', + 'delete-default-permission', + 'replace-project-default-permissions', + 'replace-content-permissions', +] as const; + +type ContentPermissionsOperation = (typeof operations)[number]; + +const granularKinds = [ + 'collection', + 'datasource', + 'project', + 'view', + 'virtualconnection', + 'workbook', +] as const satisfies readonly GranularPermissionKind[]; + +const defaultSegments = [ + 'workbooks', + 'datasources', + 'dataroles', + 'metrics', + 'flows', + 'virtualconnections', + 'databases', + 'tables', +] as const satisfies readonly DefaultPermissionSegment[]; + +const replaceProjectSegments = [ + 'dataroles', + 'databases', + 'datasources', + 'flows', + 'tables', + 'workbooks', +] as const satisfies readonly ReplaceProjectDefaultSegment[]; + +const replaceContentKinds = [ + 'datasource', + 'flow', + 'project', + 'view', + 'workbook', +] as const satisfies readonly ReplaceContentKind[]; + +const jwtScopesByOperation: Record> = { + 'list-granular-permissions': ['tableau:permissions:read'], + 'add-granular-permissions': ['tableau:permissions:update'], + 'delete-granular-permission': ['tableau:permissions:delete'], + 'list-default-permissions': ['tableau:permissions:read'], + 'add-default-permissions': ['tableau:permissions:update'], + 'delete-default-permission': ['tableau:permissions:delete'], + 'replace-project-default-permissions': ['tableau:permissions:update'], + 'replace-content-permissions': ['tableau:permissions:update'], +}; + +const paramsSchema = { + operation: z.enum(operations), + siteId: z.string().optional(), + granularKind: z + .enum(granularKinds) + .optional() + .describe( + 'For workbook ACLs use `workbook`. `resourceId` must be the workbook LUID, not the display name.', + ), + resourceId: z + .string() + .optional() + .describe( + 'Target resource LUID. For workbooks: resolve with content-workbooks (e.g. query-workbooks-for-site, filter name:eq:) then pass workbook id here.', + ), + projectId: z.string().optional(), + defaultSegment: z.enum(defaultSegments).optional(), + replaceProjectSegment: z.enum(replaceProjectSegments).optional(), + replaceContentKind: z.enum(replaceContentKinds).optional(), + granteePathKind: z.enum(['users', 'groups']).optional(), + granteeId: z.string().optional(), + capabilityName: z.string().optional(), + capabilityMode: z.string().optional(), + body: z.any().optional(), +}; + +export const getContentPermissionsTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'content-permissions', + description: + 'Tableau REST API permissions (Ask Data / lens endpoints excluded). Granular list/add/delete for collections, data sources, projects, views, virtual connections, and workbooks; default permissions per project segment; replace project defaults (API 3.23+); replace content permissions (API 3.23+). Workbook ACL recipe: use content-workbooks with operation query-workbooks-for-site and a filter (e.g. name:eq:) to get the workbook id (LUID), then list-granular-permissions with granularKind workbook and resourceId = that LUID. list-granular-permissions requires tableau:permissions:read. Results are explicit grantees (users/groups); expand groups via admin-groups/admin-users and account for inheritance (project defaults, site roles) for a full picture of who can effectively view content.', + paramsSchema, + annotations: { + title: 'Content permissions', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => { + return await invokeOperation(restApi, restApi.siteId, args); + }, + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +type Args = z.infer>; + +async function invokeOperation(restApi: RestApi, siteId: string, args: Args): Promise { + const pm = restApi.permissionsMethods; + switch (args.operation) { + case 'list-granular-permissions': + return await pm.listGranularPermissions( + siteId, + required(args.granularKind, 'granularKind'), + required(args.resourceId, 'resourceId'), + ); + case 'add-granular-permissions': + return await pm.addGranularPermissions( + siteId, + required(args.granularKind, 'granularKind'), + required(args.resourceId, 'resourceId'), + required(args.body, 'body'), + ); + case 'delete-granular-permission': + return await pm.deleteGranularPermission( + siteId, + required(args.granularKind, 'granularKind'), + required(args.resourceId, 'resourceId'), + required(args.granteePathKind, 'granteePathKind') as GranteePathKind, + required(args.granteeId, 'granteeId'), + required(args.capabilityName, 'capabilityName'), + required(args.capabilityMode, 'capabilityMode'), + ); + case 'list-default-permissions': + return await pm.listDefaultPermissions( + siteId, + required(args.projectId, 'projectId'), + required(args.defaultSegment, 'defaultSegment'), + ); + case 'add-default-permissions': + return await pm.addDefaultPermissions( + siteId, + required(args.projectId, 'projectId'), + required(args.defaultSegment, 'defaultSegment'), + required(args.body, 'body'), + ); + case 'delete-default-permission': + return await pm.deleteDefaultPermission( + siteId, + required(args.projectId, 'projectId'), + required(args.defaultSegment, 'defaultSegment'), + required(args.granteePathKind, 'granteePathKind') as GranteePathKind, + required(args.granteeId, 'granteeId'), + required(args.capabilityName, 'capabilityName'), + required(args.capabilityMode, 'capabilityMode'), + ); + case 'replace-project-default-permissions': + return await pm.replaceProjectDefaultPermissions( + siteId, + required(args.projectId, 'projectId'), + required(args.replaceProjectSegment, 'replaceProjectSegment'), + required(args.body, 'body'), + ); + case 'replace-content-permissions': + return await pm.replaceContentPermissions( + siteId, + required(args.replaceContentKind, 'replaceContentKind'), + required(args.resourceId, 'resourceId'), + required(args.body, 'body'), + ); + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null || value === '') { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/admin/users/adminUsers.ts b/src/tools/admin/users/adminUsers.ts new file mode 100644 index 000000000..e7e928a1d --- /dev/null +++ b/src/tools/admin/users/adminUsers.ts @@ -0,0 +1,189 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { Ok } from 'ts-results-es'; +import { z } from 'zod'; + +import { useRestApi } from '../../../restApiInstance.js'; +import { Server } from '../../../server.js'; +import { Tool } from '../../tool.js'; + +const operations = [ + 'add-user-to-site', + 'delete-users-from-site-with-csv', + 'download-user-credentials', + 'get-groups-for-user', + 'get-users-on-site', + 'import-users-to-site-from-csv', + 'query-user-on-site', + 'remove-user-from-site', + 'update-user', + 'upload-user-credentials', +] as const; + +type AdminUsersOperation = (typeof operations)[number]; + +const jwtScopesByOperation: Record> = { + 'add-user-to-site': ['tableau:users:create'], + 'delete-users-from-site-with-csv': ['tableau:users:delete'], + 'download-user-credentials': ['tableau:oauth_credentials:download'], + 'get-groups-for-user': ['tableau:users:read'], + 'get-users-on-site': ['tableau:users:read'], + 'import-users-to-site-from-csv': ['tableau:users:create'], + 'query-user-on-site': ['tableau:users:read'], + 'remove-user-from-site': ['tableau:users:delete'], + 'update-user': ['tableau:users:update'], + 'upload-user-credentials': ['tableau:oauth_credentials:upload'], +}; + +const paramsSchema = { + operation: z.enum(operations), + siteId: z.string().optional(), + userId: z.string().optional(), + pageSize: z.number().gt(0).optional(), + pageNumber: z.number().gt(0).optional(), + filter: z.string().optional(), + sort: z.string().optional(), + fields: z.string().optional(), + isVerbose: z.boolean().optional(), + mapAssetsTo: z.string().optional(), + body: z.any().optional(), +}; + +export const getAdminUsersTool = (server: Server): Tool => { + const tool = new Tool({ + server, + name: 'admin-users', + description: + 'Administrative Tableau users tool exposing non-SCIM user methods. Use this tool to create, update, delete, query, import, and migrate users.', + paramsSchema, + annotations: { + title: 'Admin Users', + readOnlyHint: false, + openWorldHint: false, + }, + callback: async (args, extra): Promise => { + return await tool.logAndExecute({ + extra, + args, + callback: async () => { + return new Ok( + await useRestApi({ + ...extra, + jwtScopes: jwtScopesByOperation[args.operation], + callback: async (restApi) => { + // Always use site LUID from authenticated context. + return await invokeOperation(restApi, restApi.siteId, args); + }, + }), + ); + }, + constrainSuccessResult: (result) => ({ type: 'success', result }), + }); + }, + }); + + return tool; +}; + +async function invokeOperation( + restApi: { + adminMethods: { + addUserToSite: (siteId: string, body: unknown) => Promise; + deleteUsersFromSiteWithCsv: (siteId: string, body: unknown) => Promise; + downloadUserCredentials: (siteId: string, userId: string, body: unknown) => Promise; + getGroupsForUser: ( + siteId: string, + userId: string, + queries?: { pageSize?: number; pageNumber?: number }, + ) => Promise; + getUsersOnSite: ( + siteId: string, + queries?: { + pageSize?: number; + pageNumber?: number; + filter?: string; + sort?: string; + fields?: string; + }, + ) => Promise; + importUsersToSiteFromCsv: ( + siteId: string, + body: unknown, + queries?: { isVerbose?: boolean }, + ) => Promise; + queryUserOnSite: (siteId: string, userId: string) => Promise; + removeUserFromSite: ( + siteId: string, + userId: string, + queries?: { mapAssetsTo?: string }, + ) => Promise; + updateUser: (siteId: string, userId: string, body: unknown) => Promise; + uploadUserCredentials: (siteId: string, userId: string, body: unknown) => Promise; + }; + }, + siteId: string, + args: z.objectOutputType, +): Promise { + switch (args.operation) { + case 'add-user-to-site': + return await restApi.adminMethods.addUserToSite(siteId, required(args.body, 'body')); + case 'delete-users-from-site-with-csv': + return await restApi.adminMethods.deleteUsersFromSiteWithCsv( + siteId, + required(args.body, 'body'), + ); + case 'download-user-credentials': + return await restApi.adminMethods.downloadUserCredentials( + siteId, + required(args.userId, 'userId'), + required(args.body, 'body'), + ); + case 'get-groups-for-user': + return await restApi.adminMethods.getGroupsForUser(siteId, required(args.userId, 'userId'), { + pageSize: args.pageSize, + pageNumber: args.pageNumber, + }); + case 'get-users-on-site': + return await restApi.adminMethods.getUsersOnSite(siteId, { + pageSize: args.pageSize, + pageNumber: args.pageNumber, + filter: args.filter, + sort: args.sort, + fields: args.fields, + }); + case 'import-users-to-site-from-csv': + return await restApi.adminMethods.importUsersToSiteFromCsv( + siteId, + required(args.body, 'body'), + { isVerbose: args.isVerbose }, + ); + case 'query-user-on-site': + return await restApi.adminMethods.queryUserOnSite(siteId, required(args.userId, 'userId')); + case 'remove-user-from-site': + return await restApi.adminMethods.removeUserFromSite( + siteId, + required(args.userId, 'userId'), + { + mapAssetsTo: args.mapAssetsTo, + }, + ); + case 'update-user': + return await restApi.adminMethods.updateUser( + siteId, + required(args.userId, 'userId'), + required(args.body, 'body'), + ); + case 'upload-user-credentials': + return await restApi.adminMethods.uploadUserCredentials( + siteId, + required(args.userId, 'userId'), + required(args.body, 'body'), + ); + } +} + +function required(value: T | undefined, fieldName: string): T { + if (value === undefined || value === null) { + throw new Error(`Missing required parameter: ${fieldName}`); + } + return value; +} diff --git a/src/tools/toolName.ts b/src/tools/toolName.ts index 8f0d31a77..9866437d0 100644 --- a/src/tools/toolName.ts +++ b/src/tools/toolName.ts @@ -1,49 +1,22 @@ export const toolNames = [ - 'list-datasources', - 'list-workbooks', - 'list-views', - 'query-datasource', - 'get-datasource-metadata', - 'get-workbook', - 'get-view-data', - 'get-view-image', - 'list-all-pulse-metric-definitions', - 'list-pulse-metric-definitions-from-definition-ids', - 'list-pulse-metrics-from-metric-definition-id', - 'list-pulse-metrics-from-metric-ids', - 'list-pulse-metric-subscriptions', - 'generate-pulse-metric-value-insight-bundle', - 'generate-pulse-insight-brief', - 'search-content', - 'revoke-access-token', + 'admin-users', + 'admin-groups', + 'content-permissions', + 'content-projects', + 'content-workbooks', + 'content-views', + 'site-jobs', + 'tableau-operations', ] as const; export type ToolName = (typeof toolNames)[number]; -export const toolGroupNames = [ - 'datasource', - 'workbook', - 'view', - 'pulse', - 'content-exploration', - 'token-management', -] as const; +export const toolGroupNames = ['admin', 'content', 'operations'] as const; export type ToolGroupName = (typeof toolGroupNames)[number]; export const toolGroups = { - datasource: ['list-datasources', 'get-datasource-metadata', 'query-datasource'], - workbook: ['list-workbooks', 'get-workbook'], - view: ['list-views', 'get-view-data', 'get-view-image'], - pulse: [ - 'list-all-pulse-metric-definitions', - 'list-pulse-metric-definitions-from-definition-ids', - 'list-pulse-metrics-from-metric-definition-id', - 'list-pulse-metrics-from-metric-ids', - 'list-pulse-metric-subscriptions', - 'generate-pulse-metric-value-insight-bundle', - 'generate-pulse-insight-brief', - ], - 'content-exploration': ['search-content'], - 'token-management': ['revoke-access-token'], + admin: ['admin-users', 'admin-groups'], + content: ['content-projects', 'content-workbooks', 'content-views'], + operations: ['content-permissions', 'site-jobs', 'tableau-operations'], } as const satisfies Record>; export function isToolName(value: unknown): value is ToolName { diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 5c77ae75b..a2efe2bd9 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -1,37 +1,19 @@ -import { getSearchContentTool } from './contentExploration/searchContent.js'; -import { getGetDatasourceMetadataTool } from './getDatasourceMetadata/getDatasourceMetadata.js'; -import { getListDatasourcesTool } from './listDatasources/listDatasources.js'; -import { getGeneratePulseInsightBriefTool } from './pulse/generateInsightBrief/generatePulseInsightBriefTool.js'; -import { getGeneratePulseMetricValueInsightBundleTool } from './pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.js'; -import { getListAllPulseMetricDefinitionsTool } from './pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.js'; -import { getListPulseMetricDefinitionsFromDefinitionIdsTool } from './pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.js'; -import { getListPulseMetricsFromMetricDefinitionIdTool } from './pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.js'; -import { getListPulseMetricsFromMetricIdsTool } from './pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.js'; -import { getListPulseMetricSubscriptionsTool } from './pulse/listMetricSubscriptions/listPulseMetricSubscriptions.js'; -import { getQueryDatasourceTool } from './queryDatasource/queryDatasource.js'; -import { getRevokeAccessTokenTool } from './revokeAccessToken/revokeAccessToken.js'; -import { getGetViewDataTool } from './views/getViewData.js'; -import { getGetViewImageTool } from './views/getViewImage.js'; -import { getListViewsTool } from './views/listViews.js'; -import { getGetWorkbookTool } from './workbooks/getWorkbook.js'; -import { getListWorkbooksTool } from './workbooks/listWorkbooks.js'; +import { getContentProjectsTool } from './admin/content/contentProjects.js'; +import { getContentViewsTool } from './admin/content/contentViews.js'; +import { getContentWorkbooksTool } from './admin/content/contentWorkbooks.js'; +import { getAdminGroupsTool } from './admin/groups/adminGroups.js'; +import { getSiteJobsTool } from './admin/jobs/siteJobs.js'; +import { getTableauOperationsTool } from './admin/operations/tableauOperations.js'; +import { getContentPermissionsTool } from './admin/permissions/contentPermissions.js'; +import { getAdminUsersTool } from './admin/users/adminUsers.js'; export const toolFactories = [ - getGetDatasourceMetadataTool, - getListDatasourcesTool, - getQueryDatasourceTool, - getListAllPulseMetricDefinitionsTool, - getListPulseMetricDefinitionsFromDefinitionIdsTool, - getListPulseMetricsFromMetricDefinitionIdTool, - getListPulseMetricsFromMetricIdsTool, - getListPulseMetricSubscriptionsTool, - getGeneratePulseMetricValueInsightBundleTool, - getGeneratePulseInsightBriefTool, - getGetWorkbookTool, - getGetViewDataTool, - getGetViewImageTool, - getListWorkbooksTool, - getListViewsTool, - getSearchContentTool, - getRevokeAccessTokenTool, + getAdminUsersTool, + getAdminGroupsTool, + getContentPermissionsTool, + getContentProjectsTool, + getContentWorkbooksTool, + getContentViewsTool, + getSiteJobsTool, + getTableauOperationsTool, ]; diff --git a/src/utils/uploadWorkbookToS3.ts b/src/utils/uploadWorkbookToS3.ts new file mode 100644 index 000000000..ce350d341 --- /dev/null +++ b/src/utils/uploadWorkbookToS3.ts @@ -0,0 +1,24 @@ +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; + +export type UploadTwbxParams = { + bucket: string; + key: string; + region: string; + body: Buffer; +}; + +/** + * Uploads a .twbx buffer to S3 using the process default AWS credential chain. + */ +export async function uploadTwbxToS3(params: UploadTwbxParams): Promise<{ etag?: string }> { + const client = new S3Client({ region: params.region }); + const out = await client.send( + new PutObjectCommand({ + Bucket: params.bucket, + Key: params.key, + Body: params.body, + ContentType: 'application/vnd.tableau.packaged_workbook', + }), + ); + return { etag: out.ETag }; +} From a65f12955fe8c511e9b4feb93363f7beaba4e4dd Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Tue, 14 Apr 2026 15:28:15 -0700 Subject: [PATCH 2/3] admin/: User lifecycle, group management, OAuth credentials content/: Projects, workbooks, views operations jobs/: Background job monitoring operations/: Job conflict detection, workbook archiving permissions/: Content permission management Adds tool groups that work with INCLUDE_TOOLS and EXCLUDE_TOOLS environment variables for granular access control. Tools can be filtered by individual name or group name. New tools support comprehensive site administration and content management operations through organized, scoped interfaces. --- .../configuration/mcp-config/tool-scoping.md | 40 +++++++- docs/docs/tools/admin/admin-groups.md | 70 ++++++++++++++ docs/docs/tools/admin/admin-tools-overview.md | 55 +++++++++++ docs/docs/tools/admin/admin-users.md | 81 ++++++++++++++++ docs/docs/tools/admin/tableau-operations.md | 93 ++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- src/server/oauth/scopes.ts | 95 ++++++++++++++++++- .../admin/content/contentProjects.test.ts | 23 +++++ src/tools/admin/content/contentViews.test.ts | 23 +++++ .../admin/content/contentWorkbooks.test.ts | 23 +++++ src/tools/admin/groups/adminGroups.test.ts | 23 +++++ src/tools/admin/jobs/siteJobs.test.ts | 23 +++++ .../operations/tableauOperations.test.ts | 23 +++++ .../permissions/contentPermissions.test.ts | 23 +++++ src/tools/admin/users/adminUsers.test.ts | 23 +++++ src/tools/toolName.ts | 43 ++++++++- src/tools/tools.ts | 34 +++++++ 18 files changed, 692 insertions(+), 9 deletions(-) create mode 100644 docs/docs/tools/admin/admin-groups.md create mode 100644 docs/docs/tools/admin/admin-tools-overview.md create mode 100644 docs/docs/tools/admin/admin-users.md create mode 100644 docs/docs/tools/admin/tableau-operations.md create mode 100644 src/tools/admin/content/contentProjects.test.ts create mode 100644 src/tools/admin/content/contentViews.test.ts create mode 100644 src/tools/admin/content/contentWorkbooks.test.ts create mode 100644 src/tools/admin/groups/adminGroups.test.ts create mode 100644 src/tools/admin/jobs/siteJobs.test.ts create mode 100644 src/tools/admin/operations/tableauOperations.test.ts create mode 100644 src/tools/admin/permissions/contentPermissions.test.ts create mode 100644 src/tools/admin/users/adminUsers.test.ts diff --git a/docs/docs/configuration/mcp-config/tool-scoping.md b/docs/docs/configuration/mcp-config/tool-scoping.md index 369b72626..f9c17db48 100644 --- a/docs/docs/configuration/mcp-config/tool-scoping.md +++ b/docs/docs/configuration/mcp-config/tool-scoping.md @@ -4,10 +4,44 @@ sidebar_position: 6 # Tool Scoping -The Tableau MCP server can be configured to limit the scope of its tools to a set of data sources, -workbooks, projects, or tags. +The Tableau MCP server can be configured to limit both which tools are available and the scope of data those tools can access. -Enabling tool scoping can cause: +## Tool Filtering + +You can control which tools are available using the `INCLUDE_TOOLS` and `EXCLUDE_TOOLS` environment variables. These variables accept either individual tool names or tool group names. + +### `INCLUDE_TOOLS` + +A comma-separated list of tool names or tool group names to include. When set, only the specified tools will be available. + +**Tool Groups:** +- `datasource` - Data source tools (list-datasources, get-datasource-metadata, query-datasource) +- `workbook` - Workbook tools (list-workbooks, get-workbook) +- `view` - View tools (list-views, get-view-data, get-view-image) +- `pulse` - Pulse/metrics tools +- `content-exploration` - Content search tools +- `token-management` - Token management tools +- `admin` - Admin tools for users and groups (admin-users, admin-groups) +- `content` - Content management tools (content-projects, content-workbooks, content-views) +- `operations` - Operational tools (content-permissions, site-jobs, tableau-operations) + +Example: `INCLUDE_TOOLS=datasource,workbook,admin` (enables all datasource, workbook, and admin tools) + +### `EXCLUDE_TOOLS` + +A comma-separated list of tool names or tool group names to exclude. When set, all tools except the specified ones will be available. + +Example: `EXCLUDE_TOOLS=pulse,operations` (disables all pulse and operations tools) + +:::warning +You cannot use both `INCLUDE_TOOLS` and `EXCLUDE_TOOLS` simultaneously. The server will throw an error if both are specified. +::: + +## Content Scoping + +The Tableau MCP server can also be configured to limit the scope of content accessed by tools to a set of data sources, workbooks, projects, or tags. + +Enabling content scoping can cause: 1. Tools to return an error if they are called with arguments that are not within the allowed scope. 2. Tools to respond with results that have been filtered to only include content from the allowed diff --git a/docs/docs/tools/admin/admin-groups.md b/docs/docs/tools/admin/admin-groups.md new file mode 100644 index 000000000..f1220873a --- /dev/null +++ b/docs/docs/tools/admin/admin-groups.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 3 +--- + +# admin-groups + +Administrative tool for managing Tableau groups, group sets, and group memberships. + +## Operations + +### Group Management + +- `create-group` - Create a new group +- `delete-group` - Delete a group +- `update-group` - Update group properties +- `query-groups` - List groups with filtering and pagination +- `get-group` - Get details of a specific group + +### Group Membership + +- `add-user-to-group` - Add a user to a group +- `remove-user-from-group` - Remove a user from a group +- `get-users-in-group` - List users in a group + +### Group Sets + +- `create-group-set` - Create a new group set +- `update-group-set` - Update a group set +- `delete-group-set` - Delete a group set +- `add-group-to-group-set` - Add a group to a group set +- `remove-group-from-group-set` - Remove a group from a group set + +## Parameters + +- `operation` (required) - The operation to perform +- `groupId` - Group LUID +- `userId` - User LUID (for membership operations) +- `groupSetId` - Group set LUID +- `pageSize` - Results per page +- `pageNumber` - Page number +- `filter` - Filter expression +- `sort` - Sort expression +- `body` - Request body + +## Required Scopes + +- **MCP Scope**: `tableau:mcp:admin:groups` +- **API Scopes** (operation-dependent): + - `tableau:groups:read` - Query operations + - `tableau:groups:create` - Create operations + - `tableau:groups:update` - Update operations + - `tableau:groups:delete` - Delete operations + - `tableau:users:read` - Get users in group + +## Example Usage + +```typescript +// List all groups +{ + operation: "query-groups", + pageSize: 50 +} + +// Add user to group +{ + operation: "add-user-to-group", + groupId: "group-luid", + userId: "user-luid" +} +``` diff --git a/docs/docs/tools/admin/admin-tools-overview.md b/docs/docs/tools/admin/admin-tools-overview.md new file mode 100644 index 000000000..e0b2e5c64 --- /dev/null +++ b/docs/docs/tools/admin/admin-tools-overview.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 1 +--- + +# Admin Tools Overview + +The Tableau MCP server provides a comprehensive set of administrative tools for managing users, groups, content, permissions, and site operations. These tools are designed for Tableau administrators who need programmatic access to administrative functions. + +## Tool Categories + +### User & Group Administration + +- **[admin-users](./admin-users.md)** - Manage Tableau users including creation, updates, deletion, and OAuth credential management +- **[admin-groups](./admin-groups.md)** - Manage groups, group sets, and group memberships + +### Content Management + +- **[content-projects](./content-projects.md)** - Create, update, delete, and query projects +- **[content-workbooks](./content-workbooks.md)** - Advanced workbook operations including querying, updating, deleting, and downloading +- **[content-views](./content-views.md)** - Query views and export data in multiple formats (CSV, image, PDF, Excel) + +### Permissions & Access Control + +- **[content-permissions](./content-permissions.md)** - Manage granular and default permissions for content + +### Operations & Monitoring + +- **[site-jobs](./site-jobs.md)** - Query and manage background jobs on the site +- **[tableau-operations](./tableau-operations.md)** - Advanced operational tools including job conflict detection, effective permissions analysis, and workbook archiving + +## Required Scopes + +Admin tools require specific OAuth scopes depending on the operations performed. Common scopes include: + +- `tableau:mcp:admin:users` - User administration +- `tableau:mcp:admin:groups` - Group administration +- `tableau:mcp:admin:permissions` - Permission management +- `tableau:mcp:admin:jobs` - Job monitoring +- `tableau:mcp:admin:operations` - Advanced operations + +Refer to individual tool documentation for specific scope requirements. + +## Tool Filtering + +You can control access to admin tools using the `INCLUDE_TOOLS` or `EXCLUDE_TOOLS` environment variables with tool group names: + +```bash +# Enable only admin and content tools +INCLUDE_TOOLS=admin,content + +# Disable operations tools +EXCLUDE_TOOLS=operations +``` + +See [Tool Scoping](../../configuration/mcp-config/tool-scoping.md) for more information. diff --git a/docs/docs/tools/admin/admin-users.md b/docs/docs/tools/admin/admin-users.md new file mode 100644 index 000000000..86fcdfcb9 --- /dev/null +++ b/docs/docs/tools/admin/admin-users.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 2 +--- + +# admin-users + +Administrative tool for managing Tableau users, exposing non-SCIM user methods from the Tableau REST API. + +## Operations + +### User Lifecycle + +- `add-user-to-site` - Add a new user to the site +- `remove-user-from-site` - Remove a user from the site +- `update-user` - Update user properties +- `query-user-on-site` - Query a specific user by ID +- `get-users-on-site` - List all users on the site with optional filtering, sorting, and pagination +- `get-groups-for-user` - Get groups that a user belongs to + +### Bulk Operations + +- `import-users-to-site-from-csv` - Import multiple users from CSV +- `delete-users-from-site-with-csv` - Bulk delete users using CSV + +### OAuth Credentials + +- `upload-user-credentials` - Upload OAuth credentials for a user +- `download-user-credentials` - Download OAuth credentials for a user + +## Parameters + +- `operation` (required) - The operation to perform +- `userId` - User LUID (required for user-specific operations) +- `pageSize` - Number of results per page +- `pageNumber` - Page number for pagination +- `filter` - Tableau REST API filter syntax +- `sort` - Sort expression +- `fields` - Field selection +- `body` - Request body for create/update operations + +## Required Scopes + +- **MCP Scope**: `tableau:mcp:admin:users` +- **API Scopes** (operation-dependent): + - `tableau:users:read` - Query operations + - `tableau:users:create` - Add/import operations + - `tableau:users:update` - Update operations + - `tableau:users:delete` - Delete/remove operations + - `tableau:oauth_credentials:upload` - Upload credentials + - `tableau:oauth_credentials:download` - Download credentials + +## Example Usage + +```typescript +// Get all users on the site +{ + operation: "get-users-on-site", + pageSize: 100 +} + +// Query a specific user +{ + operation: "query-user-on-site", + userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +} + +// Add a new user +{ + operation: "add-user-to-site", + body: { + user: { + name: "john.doe@example.com", + siteRole: "Viewer" + } + } +} +``` + +## References + +- [Tableau REST API - Users and Groups](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_users_and_groups.htm) diff --git a/docs/docs/tools/admin/tableau-operations.md b/docs/docs/tools/admin/tableau-operations.md new file mode 100644 index 000000000..ee64d228c --- /dev/null +++ b/docs/docs/tools/admin/tableau-operations.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 7 +--- + +# tableau-operations + +Higher-level administrative operations for Tableau Cloud including job conflict detection, permissions analysis, and workbook archiving. + +## Operations + +### Job Management + +- `get-background-job-conflicts` - Detect overlapping background jobs that may be competing for resources +- `get-job-performance-stats` - Get performance statistics for running jobs +- `kill-job-by-priority` - Cancel jobs based on priority with dry-run support + +### Permissions & Access + +- `get-effective-permissions` - Calculate effective read permissions for a specific user and workbook (heuristic analysis) +- `trace-access-reason` - Trace why a user has access to specific content +- `list-content-overrides` - List content with explicit permission overrides at the project level + +### Content Management + +- `get-stale-content-report` - Identify workbooks that haven't been accessed in a specified number of days +- `get-workbook-lineage-impact` - Analyze workbook lineage and impact using Tableau Metadata GraphQL API +- `archive-workbook` - Archive a workbook to S3 (if configured) or return as base64 + +## Parameters + +- `operation` (required) - The operation to perform +- `workbookId` - Workbook LUID +- `userId` - User LUID +- `projectFilter` - Filter for workbooks by project +- `staleDays` - Days of inactivity threshold (default from `TABLEAU_OPS_STALE_DAYS`) +- `runningThresholdMinutes` - Job runtime threshold (default from `TABLEAU_OPS_RUNNING_THRESHOLD_MINUTES`) +- `minPriority` - Minimum priority for job cancellation +- `overlapWindowMs` - Time window for detecting job overlaps (default from `TABLEAU_OPS_OVERLAP_WINDOW_MS`) +- `dryRun` - Preview actions without executing (for kill-job-by-priority) +- `maxCancels` - Maximum number of jobs to cancel +- `maxBase64Bytes` - Maximum size for base64 encoding + +## Environment Variables + +### S3 Archive Configuration + +- `TABLEAU_ARCHIVE_S3_BUCKET` - S3 bucket name for workbook archiving +- `TABLEAU_ARCHIVE_S3_REGION` - AWS region +- `TABLEAU_ARCHIVE_S3_KEY_PREFIX` - S3 key prefix + +### Operation Defaults + +- `TABLEAU_OPS_RUNNING_THRESHOLD_MINUTES` - Default threshold for flagging long-running jobs +- `TABLEAU_OPS_STALE_DAYS` - Default days of inactivity for stale content +- `TABLEAU_OPS_OVERLAP_WINDOW_MS` - Default time window for detecting job conflicts + +## Required Scopes + +- **MCP Scope**: `tableau:mcp:admin:operations` +- **API Scopes** (operation-dependent): + - `tableau:jobs:read`, `tableau:jobs:update` - Job operations + - `tableau:permissions:read`, `tableau:users:read`, `tableau:groups:read` - Permission analysis + - `tableau:content:read` - Content operations + - `tableau:views:download` - Workbook archiving + +## Example Usage + +```typescript +// Detect job conflicts +{ + operation: "get-background-job-conflicts", + overlapWindowMs: 60000 +} + +// Get effective permissions +{ + operation: "get-effective-permissions", + userId: "user-luid", + workbookId: "workbook-luid" +} + +// Archive workbook to S3 +{ + operation: "archive-workbook", + workbookId: "workbook-luid" +} +``` + +## Notes + +- `get-effective-permissions` provides a heuristic analysis for a single user/workbook pair, not a comprehensive site-wide viewer list +- For complete ACL information, use the `content-permissions` tool with `list-granular-permissions` +- Workbook archiving requires S3 environment variables to be configured; otherwise returns base64 for small workbooks diff --git a/package-lock.json b/package-lock.json index 31961c963..bf17fcc3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tableau/mcp-server", - "version": "1.18.2", + "version": "1.18.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tableau/mcp-server", - "version": "1.18.2", + "version": "1.18.3", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-s3": "^3.1030.0", diff --git a/package.json b/package.json index 55410363e..f9a1d535d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tableau/mcp-server", "description": "Helping agents see and understand data.", - "version": "1.18.2", + "version": "1.18.3", "repository": { "type": "git", "url": "git+https://github.com/tableau/tableau-mcp.git" diff --git a/src/server/oauth/scopes.ts b/src/server/oauth/scopes.ts index 29ea3c826..e8692177d 100644 --- a/src/server/oauth/scopes.ts +++ b/src/server/oauth/scopes.ts @@ -21,17 +21,38 @@ export type McpScope = | 'tableau:mcp:view:read' | 'tableau:mcp:view:download' | 'tableau:mcp:pulse:read' - | 'tableau:mcp:insight:create'; + | 'tableau:mcp:insight:create' + | 'tableau:mcp:admin:users' + | 'tableau:mcp:admin:groups' + | 'tableau:mcp:admin:permissions' + | 'tableau:mcp:admin:jobs' + | 'tableau:mcp:admin:operations'; export type TableauApiScope = | 'tableau:content:read' + | 'tableau:content:update' + | 'tableau:content:delete' | 'tableau:viz_data_service:read' | 'tableau:views:download' | 'tableau:insight_definitions_metrics:read' | 'tableau:insight_metrics:read' | 'tableau:metric_subscriptions:read' | 'tableau:insights:read' - | 'tableau:insight_brief:create'; + | 'tableau:insight_brief:create' + | 'tableau:users:read' + | 'tableau:users:create' + | 'tableau:users:update' + | 'tableau:users:delete' + | 'tableau:groups:read' + | 'tableau:groups:create' + | 'tableau:groups:update' + | 'tableau:groups:delete' + | 'tableau:permissions:read' + | 'tableau:permissions:update' + | 'tableau:jobs:read' + | 'tableau:jobs:update' + | 'tableau:oauth_credentials:download' + | 'tableau:oauth_credentials:upload'; /** * Default scopes supported by the MCP server @@ -46,6 +67,11 @@ export const DEFAULT_SCOPES_SUPPORTED: ReadonlyArray = [ 'tableau:mcp:view:download', 'tableau:mcp:pulse:read', 'tableau:mcp:insight:create', + 'tableau:mcp:admin:users', + 'tableau:mcp:admin:groups', + 'tableau:mcp:admin:permissions', + 'tableau:mcp:admin:jobs', + 'tableau:mcp:admin:operations', ]; export const RESOURCE_ACCESS_CHECKER_REQUIRED_API_SCOPES: ReadonlyArray = [ @@ -139,6 +165,71 @@ const toolScopeMap: Record< mcp: [], api: new Set(), }, + 'admin-users': { + mcp: ['tableau:mcp:admin:users'], + api: new Set([ + 'tableau:users:read', + 'tableau:users:create', + 'tableau:users:update', + 'tableau:users:delete', + 'tableau:oauth_credentials:download', + 'tableau:oauth_credentials:upload', + ]), + }, + 'admin-groups': { + mcp: ['tableau:mcp:admin:groups'], + api: new Set([ + 'tableau:groups:read', + 'tableau:groups:create', + 'tableau:groups:update', + 'tableau:groups:delete', + 'tableau:users:read', + ]), + }, + 'content-permissions': { + mcp: ['tableau:mcp:admin:permissions'], + api: new Set([ + 'tableau:permissions:read', + 'tableau:permissions:update', + 'tableau:content:read', + ]), + }, + 'content-projects': { + mcp: ['tableau:mcp:content:read'], + api: new Set([ + 'tableau:content:read', + 'tableau:content:update', + 'tableau:content:delete', + ]), + }, + 'content-workbooks': { + mcp: ['tableau:mcp:workbook:read'], + api: new Set([ + 'tableau:content:read', + 'tableau:content:update', + 'tableau:content:delete', + 'tableau:views:download', + ]), + }, + 'content-views': { + mcp: ['tableau:mcp:view:read'], + api: new Set(['tableau:content:read', 'tableau:views:download']), + }, + 'site-jobs': { + mcp: ['tableau:mcp:admin:jobs'], + api: new Set(['tableau:jobs:read']), + }, + 'tableau-operations': { + mcp: ['tableau:mcp:admin:operations'], + api: new Set([ + 'tableau:jobs:read', + 'tableau:jobs:update', + 'tableau:content:read', + 'tableau:permissions:read', + 'tableau:users:read', + 'tableau:groups:read', + ]), + }, }; const supportedMcpScopes = Array.from( diff --git a/src/tools/admin/content/contentProjects.test.ts b/src/tools/admin/content/contentProjects.test.ts new file mode 100644 index 000000000..475a58ecc --- /dev/null +++ b/src/tools/admin/content/contentProjects.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getContentProjectsTool } from './contentProjects.js'; + +describe('content-projects tool', () => { + it('should have correct tool name', () => { + const tool = getContentProjectsTool(new Server()); + expect(tool.name).toBe('content-projects'); + }); + + it('should have correct annotations', () => { + const tool = getContentProjectsTool(new Server()); + expect(tool.annotations?.title).toBe('Content — Projects'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getContentProjectsTool(new Server()); + expect(tool.description).toContain('Tableau site projects'); + }); +}); diff --git a/src/tools/admin/content/contentViews.test.ts b/src/tools/admin/content/contentViews.test.ts new file mode 100644 index 000000000..06457f64f --- /dev/null +++ b/src/tools/admin/content/contentViews.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getContentViewsTool } from './contentViews.js'; + +describe('content-views tool', () => { + it('should have correct tool name', () => { + const tool = getContentViewsTool(new Server()); + expect(tool.name).toBe('content-views'); + }); + + it('should have correct annotations', () => { + const tool = getContentViewsTool(new Server()); + expect(tool.annotations?.title).toBe('Content — Views'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getContentViewsTool(new Server()); + expect(tool.description).toContain('Tableau views'); + }); +}); diff --git a/src/tools/admin/content/contentWorkbooks.test.ts b/src/tools/admin/content/contentWorkbooks.test.ts new file mode 100644 index 000000000..07e299b6f --- /dev/null +++ b/src/tools/admin/content/contentWorkbooks.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getContentWorkbooksTool } from './contentWorkbooks.js'; + +describe('content-workbooks tool', () => { + it('should have correct tool name', () => { + const tool = getContentWorkbooksTool(new Server()); + expect(tool.name).toBe('content-workbooks'); + }); + + it('should have correct annotations', () => { + const tool = getContentWorkbooksTool(new Server()); + expect(tool.annotations?.title).toBe('Content — Workbooks'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getContentWorkbooksTool(new Server()); + expect(tool.description).toContain('Tableau workbooks on the site'); + }); +}); diff --git a/src/tools/admin/groups/adminGroups.test.ts b/src/tools/admin/groups/adminGroups.test.ts new file mode 100644 index 000000000..3911b0c21 --- /dev/null +++ b/src/tools/admin/groups/adminGroups.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getAdminGroupsTool } from './adminGroups.js'; + +describe('admin-groups tool', () => { + it('should have correct tool name', () => { + const tool = getAdminGroupsTool(new Server()); + expect(tool.name).toBe('admin-groups'); + }); + + it('should have correct annotations', () => { + const tool = getAdminGroupsTool(new Server()); + expect(tool.annotations?.title).toBe('Admin Groups'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getAdminGroupsTool(new Server()); + expect(tool.description).toContain('Administrative Tableau groups'); + }); +}); diff --git a/src/tools/admin/jobs/siteJobs.test.ts b/src/tools/admin/jobs/siteJobs.test.ts new file mode 100644 index 000000000..7ab3901c2 --- /dev/null +++ b/src/tools/admin/jobs/siteJobs.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getSiteJobsTool } from './siteJobs.js'; + +describe('site-jobs tool', () => { + it('should have correct tool name', () => { + const tool = getSiteJobsTool(new Server()); + expect(tool.name).toBe('site-jobs'); + }); + + it('should have correct annotations', () => { + const tool = getSiteJobsTool(new Server()); + expect(tool.annotations?.title).toBe('Site jobs'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getSiteJobsTool(new Server()); + expect(tool.description).toContain('Query active background jobs'); + }); +}); diff --git a/src/tools/admin/operations/tableauOperations.test.ts b/src/tools/admin/operations/tableauOperations.test.ts new file mode 100644 index 000000000..b4c597a4b --- /dev/null +++ b/src/tools/admin/operations/tableauOperations.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getTableauOperationsTool } from './tableauOperations.js'; + +describe('tableau-operations tool', () => { + it('should have correct tool name', () => { + const tool = getTableauOperationsTool(new Server()); + expect(tool.name).toBe('tableau-operations'); + }); + + it('should have correct annotations', () => { + const tool = getTableauOperationsTool(new Server()); + expect(tool.annotations?.title).toBe('Tableau operations'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(true); + }); + + it('should have correct description', () => { + const tool = getTableauOperationsTool(new Server()); + expect(tool.description).toContain('Higher-level Tableau Cloud operations'); + }); +}); diff --git a/src/tools/admin/permissions/contentPermissions.test.ts b/src/tools/admin/permissions/contentPermissions.test.ts new file mode 100644 index 000000000..6ba013edf --- /dev/null +++ b/src/tools/admin/permissions/contentPermissions.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getContentPermissionsTool } from './contentPermissions.js'; + +describe('content-permissions tool', () => { + it('should have correct tool name', () => { + const tool = getContentPermissionsTool(new Server()); + expect(tool.name).toBe('content-permissions'); + }); + + it('should have correct annotations', () => { + const tool = getContentPermissionsTool(new Server()); + expect(tool.annotations?.title).toBe('Content permissions'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getContentPermissionsTool(new Server()); + expect(tool.description).toContain('Tableau REST API permissions'); + }); +}); diff --git a/src/tools/admin/users/adminUsers.test.ts b/src/tools/admin/users/adminUsers.test.ts new file mode 100644 index 000000000..409e3731a --- /dev/null +++ b/src/tools/admin/users/adminUsers.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { Server } from '../../../server.js'; +import { getAdminUsersTool } from './adminUsers.js'; + +describe('admin-users tool', () => { + it('should have correct tool name', () => { + const tool = getAdminUsersTool(new Server()); + expect(tool.name).toBe('admin-users'); + }); + + it('should have correct annotations', () => { + const tool = getAdminUsersTool(new Server()); + expect(tool.annotations?.title).toBe('Admin Users'); + expect(tool.annotations?.readOnlyHint).toBe(false); + expect(tool.annotations?.openWorldHint).toBe(false); + }); + + it('should have correct description', () => { + const tool = getAdminUsersTool(new Server()); + expect(tool.description).toContain('Administrative Tableau users tool'); + }); +}); diff --git a/src/tools/toolName.ts b/src/tools/toolName.ts index 9866437d0..618d4e582 100644 --- a/src/tools/toolName.ts +++ b/src/tools/toolName.ts @@ -1,4 +1,21 @@ export const toolNames = [ + 'list-datasources', + 'list-workbooks', + 'list-views', + 'query-datasource', + 'get-datasource-metadata', + 'get-workbook', + 'get-view-data', + 'get-view-image', + 'list-all-pulse-metric-definitions', + 'list-pulse-metric-definitions-from-definition-ids', + 'list-pulse-metrics-from-metric-definition-id', + 'list-pulse-metrics-from-metric-ids', + 'list-pulse-metric-subscriptions', + 'generate-pulse-metric-value-insight-bundle', + 'generate-pulse-insight-brief', + 'search-content', + 'revoke-access-token', 'admin-users', 'admin-groups', 'content-permissions', @@ -10,10 +27,34 @@ export const toolNames = [ ] as const; export type ToolName = (typeof toolNames)[number]; -export const toolGroupNames = ['admin', 'content', 'operations'] as const; +export const toolGroupNames = [ + 'datasource', + 'workbook', + 'view', + 'pulse', + 'content-exploration', + 'token-management', + 'admin', + 'content', + 'operations', +] as const; export type ToolGroupName = (typeof toolGroupNames)[number]; export const toolGroups = { + datasource: ['list-datasources', 'get-datasource-metadata', 'query-datasource'], + workbook: ['list-workbooks', 'get-workbook'], + view: ['list-views', 'get-view-data', 'get-view-image'], + pulse: [ + 'list-all-pulse-metric-definitions', + 'list-pulse-metric-definitions-from-definition-ids', + 'list-pulse-metrics-from-metric-definition-id', + 'list-pulse-metrics-from-metric-ids', + 'list-pulse-metric-subscriptions', + 'generate-pulse-metric-value-insight-bundle', + 'generate-pulse-insight-brief', + ], + 'content-exploration': ['search-content'], + 'token-management': ['revoke-access-token'], admin: ['admin-users', 'admin-groups'], content: ['content-projects', 'content-workbooks', 'content-views'], operations: ['content-permissions', 'site-jobs', 'tableau-operations'], diff --git a/src/tools/tools.ts b/src/tools/tools.ts index a2efe2bd9..4776c79bd 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -6,8 +6,42 @@ import { getSiteJobsTool } from './admin/jobs/siteJobs.js'; import { getTableauOperationsTool } from './admin/operations/tableauOperations.js'; import { getContentPermissionsTool } from './admin/permissions/contentPermissions.js'; import { getAdminUsersTool } from './admin/users/adminUsers.js'; +import { getSearchContentTool } from './contentExploration/searchContent.js'; +import { getGetDatasourceMetadataTool } from './getDatasourceMetadata/getDatasourceMetadata.js'; +import { getListDatasourcesTool } from './listDatasources/listDatasources.js'; +import { getGeneratePulseInsightBriefTool } from './pulse/generateInsightBrief/generatePulseInsightBriefTool.js'; +import { getGeneratePulseMetricValueInsightBundleTool } from './pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.js'; +import { getListAllPulseMetricDefinitionsTool } from './pulse/listAllMetricDefinitions/listAllPulseMetricDefinitions.js'; +import { getListPulseMetricDefinitionsFromDefinitionIdsTool } from './pulse/listMetricDefinitionsFromDefinitionIds/listPulseMetricDefinitionsFromDefinitionIds.js'; +import { getListPulseMetricsFromMetricDefinitionIdTool } from './pulse/listMetricsFromMetricDefinitionId/listPulseMetricsFromMetricDefinitionId.js'; +import { getListPulseMetricsFromMetricIdsTool } from './pulse/listMetricsFromMetricIds/listPulseMetricsFromMetricIds.js'; +import { getListPulseMetricSubscriptionsTool } from './pulse/listMetricSubscriptions/listPulseMetricSubscriptions.js'; +import { getQueryDatasourceTool } from './queryDatasource/queryDatasource.js'; +import { getRevokeAccessTokenTool } from './revokeAccessToken/revokeAccessToken.js'; +import { getGetViewDataTool } from './views/getViewData.js'; +import { getGetViewImageTool } from './views/getViewImage.js'; +import { getListViewsTool } from './views/listViews.js'; +import { getGetWorkbookTool } from './workbooks/getWorkbook.js'; +import { getListWorkbooksTool } from './workbooks/listWorkbooks.js'; export const toolFactories = [ + getGetDatasourceMetadataTool, + getListDatasourcesTool, + getQueryDatasourceTool, + getListAllPulseMetricDefinitionsTool, + getListPulseMetricDefinitionsFromDefinitionIdsTool, + getListPulseMetricsFromMetricDefinitionIdTool, + getListPulseMetricsFromMetricIdsTool, + getListPulseMetricSubscriptionsTool, + getGeneratePulseMetricValueInsightBundleTool, + getGeneratePulseInsightBriefTool, + getGetWorkbookTool, + getGetViewDataTool, + getGetViewImageTool, + getListWorkbooksTool, + getListViewsTool, + getSearchContentTool, + getRevokeAccessTokenTool, getAdminUsersTool, getAdminGroupsTool, getContentPermissionsTool, From 6f013d5cbeb1ba2f371fed52cd7f1e32f446716f Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Fri, 17 Apr 2026 12:40:20 -0500 Subject: [PATCH 3/3] tests --- .../admin/content/contentProjects.test.ts | 62 ++- .../admin/content/contentWorkbooks.test.ts | 49 ++- src/tools/admin/groups/adminGroups.test.ts | 151 ++++++- src/tools/admin/users/adminUsers.test.ts | 390 +++++++++++++++++- 4 files changed, 614 insertions(+), 38 deletions(-) diff --git a/src/tools/admin/content/contentProjects.test.ts b/src/tools/admin/content/contentProjects.test.ts index 475a58ecc..6f9f7ef14 100644 --- a/src/tools/admin/content/contentProjects.test.ts +++ b/src/tools/admin/content/contentProjects.test.ts @@ -1,23 +1,69 @@ -import { describe, expect, it } from 'vitest'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { Server } from '../../../server.js'; +import { stubDefaultEnvVars } from '../../../testShared.js'; +import { getMockRequestHandlerExtra } from '../../toolContext.mock.js'; import { getContentProjectsTool } from './contentProjects.js'; +const mocks = vi.hoisted(() => ({ + mockQueryProjects: vi.fn(), + mockCreateProject: vi.fn(), + mockUpdateProject: vi.fn(), + mockDeleteProject: vi.fn(), +})); + +vi.mock('../../../restApiInstance.js', () => ({ + useRestApi: vi.fn().mockImplementation(async ({ callback }) => + callback({ + projectsMethods: { + queryProjects: mocks.mockQueryProjects, + createProject: mocks.mockCreateProject, + updateProject: mocks.mockUpdateProject, + deleteProject: mocks.mockDeleteProject, + }, + siteId: 'test-site-id', + }), + ), +})); + describe('content-projects tool', () => { - it('should have correct tool name', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + stubDefaultEnvVars(); + }); + + it('should create a tool instance', () => { const tool = getContentProjectsTool(new Server()); expect(tool.name).toBe('content-projects'); }); - it('should have correct annotations', () => { + it('should query projects', async () => { + mocks.mockQueryProjects.mockResolvedValue({ + projects: { project: [{ id: 'proj-1', name: 'Project 1' }] }, + }); + const tool = getContentProjectsTool(new Server()); - expect(tool.annotations?.title).toBe('Content — Projects'); - expect(tool.annotations?.readOnlyHint).toBe(false); - expect(tool.annotations?.openWorldHint).toBe(false); + await tool.callback( + { operation: 'query-projects', filter: 'name:eq:Sales' }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockQueryProjects).toHaveBeenCalled(); }); - it('should have correct description', () => { + it('should create project', async () => { + mocks.mockCreateProject.mockResolvedValue({ project: { id: 'proj-123' } }); + const tool = getContentProjectsTool(new Server()); - expect(tool.description).toContain('Tableau site projects'); + await tool.callback( + { + operation: 'create-project', + body: { project: { name: 'New Project' } }, + }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockCreateProject).toHaveBeenCalled(); }); }); diff --git a/src/tools/admin/content/contentWorkbooks.test.ts b/src/tools/admin/content/contentWorkbooks.test.ts index 07e299b6f..f86e88d8c 100644 --- a/src/tools/admin/content/contentWorkbooks.test.ts +++ b/src/tools/admin/content/contentWorkbooks.test.ts @@ -1,23 +1,50 @@ -import { describe, expect, it } from 'vitest'; - import { Server } from '../../../server.js'; +import { stubDefaultEnvVars } from '../../../testShared.js'; +import { getMockRequestHandlerExtra } from '../../toolContext.mock.js'; import { getContentWorkbooksTool } from './contentWorkbooks.js'; +const mocks = vi.hoisted(() => ({ + mockGetWorkbook: vi.fn(), + mockQueryWorkbooksForSite: vi.fn(), + mockQueryWorkbooksForUser: vi.fn(), + mockUpdateWorkbook: vi.fn(), + mockDeleteWorkbook: vi.fn(), + mockDownloadWorkbook: vi.fn(), +})); + +vi.mock('../../../restApiInstance.js', () => ({ + useRestApi: vi.fn().mockImplementation(async ({ callback }) => + callback({ + workbooksMethods: { + getWorkbook: mocks.mockGetWorkbook, + queryWorkbooksForSite: mocks.mockQueryWorkbooksForSite, + queryWorkbooksForUser: mocks.mockQueryWorkbooksForUser, + updateWorkbook: mocks.mockUpdateWorkbook, + deleteWorkbook: mocks.mockDeleteWorkbook, + downloadWorkbook: mocks.mockDownloadWorkbook, + }, + siteId: 'test-site-id', + }), + ), +})); + describe('content-workbooks tool', () => { - it('should have correct tool name', () => { - const tool = getContentWorkbooksTool(new Server()); - expect(tool.name).toBe('content-workbooks'); + beforeEach(() => { + vi.clearAllMocks(); + stubDefaultEnvVars(); }); - it('should have correct annotations', () => { + it('should create tool instance', () => { const tool = getContentWorkbooksTool(new Server()); - expect(tool.annotations?.title).toBe('Content — Workbooks'); - expect(tool.annotations?.readOnlyHint).toBe(false); - expect(tool.annotations?.openWorldHint).toBe(false); + expect(tool.name).toBe('content-workbooks'); }); - it('should have correct description', () => { + it('should query workbooks for site', async () => { + mocks.mockQueryWorkbooksForSite.mockResolvedValue({ workbooks: { workbook: [] } }); + const tool = getContentWorkbooksTool(new Server()); - expect(tool.description).toContain('Tableau workbooks on the site'); + await tool.callback({ operation: 'query-workbooks-for-site' }, getMockRequestHandlerExtra()); + + expect(mocks.mockQueryWorkbooksForSite).toHaveBeenCalled(); }); }); diff --git a/src/tools/admin/groups/adminGroups.test.ts b/src/tools/admin/groups/adminGroups.test.ts index 3911b0c21..2b29b2639 100644 --- a/src/tools/admin/groups/adminGroups.test.ts +++ b/src/tools/admin/groups/adminGroups.test.ts @@ -1,23 +1,154 @@ -import { describe, expect, it } from 'vitest'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { Server } from '../../../server.js'; +import { stubDefaultEnvVars } from '../../../testShared.js'; +import { getMockRequestHandlerExtra } from '../../toolContext.mock.js'; import { getAdminGroupsTool } from './adminGroups.js'; +const mocks = vi.hoisted(() => ({ + mockCreateGroup: vi.fn(), + mockDeleteGroup: vi.fn(), + mockUpdateGroup: vi.fn(), + mockQueryGroups: vi.fn(), + mockAddUserToGroup: vi.fn(), + mockRemoveUserFromGroup: vi.fn(), + mockBulkRemoveUsersFromGroup: vi.fn(), + mockGetUsersInGroup: vi.fn(), + mockCreateGroupSet: vi.fn(), + mockUpdateGroupSet: vi.fn(), + mockDeleteGroupSet: vi.fn(), + mockGetGroupSet: vi.fn(), + mockListGroupSets: vi.fn(), + mockAddGroupToGroupSet: vi.fn(), + mockRemoveGroupFromGroupSet: vi.fn(), +})); + +vi.mock('../../../restApiInstance.js', () => ({ + useRestApi: vi.fn().mockImplementation(async ({ callback }) => + callback({ + adminMethods: { + createGroup: mocks.mockCreateGroup, + deleteGroup: mocks.mockDeleteGroup, + updateGroup: mocks.mockUpdateGroup, + queryGroups: mocks.mockQueryGroups, + addUserToGroup: mocks.mockAddUserToGroup, + removeUserFromGroup: mocks.mockRemoveUserFromGroup, + bulkRemoveUsersFromGroup: mocks.mockBulkRemoveUsersFromGroup, + getUsersInGroup: mocks.mockGetUsersInGroup, + createGroupSet: mocks.mockCreateGroupSet, + updateGroupSet: mocks.mockUpdateGroupSet, + deleteGroupSet: mocks.mockDeleteGroupSet, + getGroupSet: mocks.mockGetGroupSet, + listGroupSets: mocks.mockListGroupSets, + addGroupToGroupSet: mocks.mockAddGroupToGroupSet, + removeGroupFromGroupSet: mocks.mockRemoveGroupFromGroupSet, + }, + siteId: 'test-site-id', + }), + ), +})); + describe('admin-groups tool', () => { - it('should have correct tool name', () => { - const tool = getAdminGroupsTool(new Server()); - expect(tool.name).toBe('admin-groups'); + beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + stubDefaultEnvVars(); }); - it('should have correct annotations', () => { - const tool = getAdminGroupsTool(new Server()); - expect(tool.annotations?.title).toBe('Admin Groups'); - expect(tool.annotations?.readOnlyHint).toBe(false); - expect(tool.annotations?.openWorldHint).toBe(false); + afterEach(() => { + vi.unstubAllEnvs(); }); - it('should have correct description', () => { + it('should create a tool instance with correct properties', () => { const tool = getAdminGroupsTool(new Server()); + expect(tool.name).toBe('admin-groups'); expect(tool.description).toContain('Administrative Tableau groups'); }); + + describe('create-group', () => { + it('should create a group', async () => { + const mockGroup = { group: { id: 'group-123', name: 'Analysts' } }; + mocks.mockCreateGroup.mockResolvedValue(mockGroup); + + const tool = getAdminGroupsTool(new Server()); + const result = (await tool.callback( + { + operation: 'create-group', + body: { group: { name: 'Analysts' } }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockCreateGroup).toHaveBeenCalledWith( + 'test-site-id', + { group: { name: 'Analysts' } }, + { asJob: undefined }, + ); + }); + }); + + describe('query-groups', () => { + it('should query groups with pagination', async () => { + const mockGroups = { + groups: { group: [{ id: 'group-1', name: 'Group 1' }] }, + }; + mocks.mockQueryGroups.mockResolvedValue(mockGroups); + + const tool = getAdminGroupsTool(new Server()); + await tool.callback( + { + operation: 'query-groups', + pageSize: 10, + filter: 'name:eq:Analysts', + }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockQueryGroups).toHaveBeenCalledWith('test-site-id', { + pageSize: 10, + filter: 'name:eq:Analysts', + }); + }); + }); + + describe('add-user-to-group', () => { + it('should add user to group', async () => { + mocks.mockAddUserToGroup.mockResolvedValue({ user: { id: 'user-123' } }); + + const tool = getAdminGroupsTool(new Server()); + await tool.callback( + { + operation: 'add-user-to-group', + groupId: 'group-123', + body: { user: { id: 'user-123' } }, + }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockAddUserToGroup).toHaveBeenCalledWith( + 'test-site-id', + 'group-123', + { user: { id: 'user-123' } }, + ); + }); + }); + + describe('get-users-in-group', () => { + it('should get users in group', async () => { + const mockUsers = { users: { user: [{ id: 'user-1' }] } }; + mocks.mockGetUsersInGroup.mockResolvedValue(mockUsers); + + const tool = getAdminGroupsTool(new Server()); + await tool.callback( + { + operation: 'get-users-in-group', + groupId: 'group-123', + }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockGetUsersInGroup).toHaveBeenCalledWith('test-site-id', 'group-123', {}); + }); + }); }); diff --git a/src/tools/admin/users/adminUsers.test.ts b/src/tools/admin/users/adminUsers.test.ts index 409e3731a..897b31ca9 100644 --- a/src/tools/admin/users/adminUsers.test.ts +++ b/src/tools/admin/users/adminUsers.test.ts @@ -1,23 +1,395 @@ -import { describe, expect, it } from 'vitest'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { Server } from '../../../server.js'; +import { stubDefaultEnvVars } from '../../../testShared.js'; +import { getMockRequestHandlerExtra } from '../../toolContext.mock.js'; import { getAdminUsersTool } from './adminUsers.js'; +const mocks = vi.hoisted(() => ({ + mockAddUserToSite: vi.fn(), + mockDeleteUsersFromSiteWithCsv: vi.fn(), + mockDownloadUserCredentials: vi.fn(), + mockGetGroupsForUser: vi.fn(), + mockGetUsersOnSite: vi.fn(), + mockImportUsersToSiteFromCsv: vi.fn(), + mockQueryUserOnSite: vi.fn(), + mockRemoveUserFromSite: vi.fn(), + mockUpdateUser: vi.fn(), + mockUploadUserCredentials: vi.fn(), +})); + +vi.mock('../../../restApiInstance.js', () => ({ + useRestApi: vi.fn().mockImplementation(async ({ callback }) => + callback({ + adminMethods: { + addUserToSite: mocks.mockAddUserToSite, + deleteUsersFromSiteWithCsv: mocks.mockDeleteUsersFromSiteWithCsv, + downloadUserCredentials: mocks.mockDownloadUserCredentials, + getGroupsForUser: mocks.mockGetGroupsForUser, + getUsersOnSite: mocks.mockGetUsersOnSite, + importUsersToSiteFromCsv: mocks.mockImportUsersToSiteFromCsv, + queryUserOnSite: mocks.mockQueryUserOnSite, + removeUserFromSite: mocks.mockRemoveUserFromSite, + updateUser: mocks.mockUpdateUser, + uploadUserCredentials: mocks.mockUploadUserCredentials, + }, + siteId: 'test-site-id', + }), + ), +})); + describe('admin-users tool', () => { - it('should have correct tool name', () => { - const tool = getAdminUsersTool(new Server()); - expect(tool.name).toBe('admin-users'); + beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + stubDefaultEnvVars(); }); - it('should have correct annotations', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('should create a tool instance with correct properties', () => { const tool = getAdminUsersTool(new Server()); + expect(tool.name).toBe('admin-users'); + expect(tool.description).toContain('Administrative Tableau users tool'); expect(tool.annotations?.title).toBe('Admin Users'); expect(tool.annotations?.readOnlyHint).toBe(false); - expect(tool.annotations?.openWorldHint).toBe(false); }); - it('should have correct description', () => { - const tool = getAdminUsersTool(new Server()); - expect(tool.description).toContain('Administrative Tableau users tool'); + describe('add-user-to-site', () => { + it('should add a user to the site', async () => { + const mockUser = { + id: 'user-123', + name: 'john.doe@example.com', + siteRole: 'Viewer', + }; + mocks.mockAddUserToSite.mockResolvedValue({ user: mockUser }); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'add-user-to-site', + body: { + user: { + name: 'john.doe@example.com', + siteRole: 'Viewer', + }, + }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockAddUserToSite).toHaveBeenCalledWith('test-site-id', { + user: { + name: 'john.doe@example.com', + siteRole: 'Viewer', + }, + }); + }); + }); + + describe('get-users-on-site', () => { + it('should get users on site with pagination', async () => { + const mockUsers = { + users: { + user: [ + { id: 'user-1', name: 'user1@example.com' }, + { id: 'user-2', name: 'user2@example.com' }, + ], + }, + }; + mocks.mockGetUsersOnSite.mockResolvedValue(mockUsers); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'get-users-on-site', + pageSize: 10, + pageNumber: 1, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockGetUsersOnSite).toHaveBeenCalledWith('test-site-id', { + pageSize: 10, + pageNumber: 1, + }); + }); + + it('should get users with filter and sort', async () => { + const mockUsers = { users: { user: [] } }; + mocks.mockGetUsersOnSite.mockResolvedValue(mockUsers); + + const tool = getAdminUsersTool(new Server()); + await tool.callback( + { + operation: 'get-users-on-site', + filter: 'name:eq:test@example.com', + sort: 'name:asc', + }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockGetUsersOnSite).toHaveBeenCalledWith('test-site-id', { + filter: 'name:eq:test@example.com', + sort: 'name:asc', + }); + }); + }); + + describe('query-user-on-site', () => { + it('should query a specific user', async () => { + const mockUser = { + user: { + id: 'user-123', + name: 'john.doe@example.com', + siteRole: 'Viewer', + }, + }; + mocks.mockQueryUserOnSite.mockResolvedValue(mockUser); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'query-user-on-site', + userId: 'user-123', + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockQueryUserOnSite).toHaveBeenCalledWith('test-site-id', 'user-123'); + }); + }); + + describe('update-user', () => { + it('should update a user', async () => { + const mockUpdatedUser = { + user: { + id: 'user-123', + name: 'john.doe@example.com', + siteRole: 'Explorer', + }, + }; + mocks.mockUpdateUser.mockResolvedValue(mockUpdatedUser); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'update-user', + userId: 'user-123', + body: { + user: { + siteRole: 'Explorer', + }, + }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockUpdateUser).toHaveBeenCalledWith('test-site-id', 'user-123', { + user: { + siteRole: 'Explorer', + }, + }); + }); + }); + + describe('remove-user-from-site', () => { + it('should remove a user from the site', async () => { + mocks.mockRemoveUserFromSite.mockResolvedValue({}); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'remove-user-from-site', + userId: 'user-123', + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockRemoveUserFromSite).toHaveBeenCalledWith('test-site-id', 'user-123', {}); + }); + + it('should remove user with mapAssetsTo parameter', async () => { + mocks.mockRemoveUserFromSite.mockResolvedValue({}); + + const tool = getAdminUsersTool(new Server()); + await tool.callback( + { + operation: 'remove-user-from-site', + userId: 'user-123', + mapAssetsTo: 'user-456', + }, + getMockRequestHandlerExtra(), + ); + + expect(mocks.mockRemoveUserFromSite).toHaveBeenCalledWith('test-site-id', 'user-123', { + mapAssetsTo: 'user-456', + }); + }); + }); + + describe('get-groups-for-user', () => { + it('should get groups for a user', async () => { + const mockGroups = { + groups: { + group: [ + { id: 'group-1', name: 'Analysts' }, + { id: 'group-2', name: 'Developers' }, + ], + }, + }; + mocks.mockGetGroupsForUser.mockResolvedValue(mockGroups); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'get-groups-for-user', + userId: 'user-123', + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockGetGroupsForUser).toHaveBeenCalledWith('test-site-id', 'user-123', {}); + }); + }); + + describe('import-users-to-site-from-csv', () => { + it('should import users from CSV', async () => { + const mockImportResult = { + tableauCredentials: { + site: { id: 'site-123' }, + }, + }; + mocks.mockImportUsersToSiteFromCsv.mockResolvedValue(mockImportResult); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'import-users-to-site-from-csv', + body: { + userImport: { + csv: 'username,password,display_name\nuser1,pass1,User One', + }, + }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockImportUsersToSiteFromCsv).toHaveBeenCalledWith( + 'test-site-id', + { + userImport: { + csv: 'username,password,display_name\nuser1,pass1,User One', + }, + }, + { isVerbose: undefined }, + ); + }); + }); + + describe('delete-users-from-site-with-csv', () => { + it('should delete users from CSV', async () => { + mocks.mockDeleteUsersFromSiteWithCsv.mockResolvedValue({}); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'delete-users-from-site-with-csv', + body: { + userDelete: { + csv: 'username\nuser1@example.com\nuser2@example.com', + }, + }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockDeleteUsersFromSiteWithCsv).toHaveBeenCalledWith('test-site-id', { + userDelete: { + csv: 'username\nuser1@example.com\nuser2@example.com', + }, + }); + }); + }); + + describe('OAuth credentials', () => { + it('should upload user credentials', async () => { + const mockCredentials = { + credentials: { + clientId: 'client-123', + }, + }; + mocks.mockUploadUserCredentials.mockResolvedValue(mockCredentials); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'upload-user-credentials', + userId: 'user-123', + body: { + credentials: { + name: 'My OAuth Client', + }, + }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockUploadUserCredentials).toHaveBeenCalledWith( + 'test-site-id', + 'user-123', + { + credentials: { + name: 'My OAuth Client', + }, + }, + ); + }); + + it('should download user credentials', async () => { + const mockCredentials = { + credentials: { + clientId: 'client-123', + clientSecret: 'secret-456', + }, + }; + mocks.mockDownloadUserCredentials.mockResolvedValue(mockCredentials); + + const tool = getAdminUsersTool(new Server()); + const result = (await tool.callback( + { + operation: 'download-user-credentials', + userId: 'user-123', + body: { + credentials: { + name: 'My OAuth Client', + }, + }, + }, + getMockRequestHandlerExtra(), + )) as CallToolResult; + + expect(result.isError).toBe(false); + expect(mocks.mockDownloadUserCredentials).toHaveBeenCalledWith( + 'test-site-id', + 'user-123', + { + credentials: { + name: 'My OAuth Client', + }, + }, + ); + }); }); });