From 2d678c3af43c15f4f91c9a40f6ad4248c806cdde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 3 Jan 2026 00:23:03 +0300 Subject: [PATCH 1/4] enterprise kms support added, microservice volumeMount via type (prefix removed) --- package-lock.json | 4928 ++++++++++++----- package.json | 6 +- src/config/controller.yaml | 29 + src/config/env-mapping.js | 22 + src/data/managers/config-map-manager.js | 38 +- src/data/managers/iofog-public-key-manager.js | 5 + src/data/managers/secret-manager.js | 6 +- ....0.6.sql => db_migration_mysql_v1.0.7.sql} | 2 + ..._v1.0.6.sql => db_migration_pg_v1.0.7.sql} | 1 + ...0.6.sql => db_migration_sqlite_v1.0.7.sql} | 1 + src/data/models/configMap.js | 32 +- src/data/models/secret.js | 16 +- src/data/providers/database-provider.js | 12 +- src/helpers/error-messages.js | 3 +- src/helpers/secret-helper.js | 107 +- src/init.js | 8 + src/schemas/config-map.js | 4 + src/schemas/microservice.js | 2 +- src/services/agent-service.js | 12 +- src/services/config-map-service.js | 31 +- src/services/iofog-key-service.js | 28 +- src/services/iofog-service.js | 12 + src/services/microservices-service.js | 47 +- src/services/router-connection-service.js | 2 +- src/services/router-service.js | 9 +- src/services/secret-service.js | 30 +- src/services/yaml-parser-service.js | 18 +- src/vault/aws-secrets-manager-provider.js | 191 + src/vault/azure-key-vault-provider.js | 166 + src/vault/base-vault-provider.js | 148 + src/vault/google-secret-manager-provider.js | 222 + src/vault/hashicorp-vault-provider.js | 231 + src/vault/vault-manager.js | 305 + 33 files changed, 5244 insertions(+), 1430 deletions(-) rename src/data/migrations/mysql/{db_migration_mysql_v1.0.6.sql => db_migration_mysql_v1.0.7.sql} (99%) rename src/data/migrations/postgres/{db_migration_pg_v1.0.6.sql => db_migration_pg_v1.0.7.sql} (99%) rename src/data/migrations/sqlite/{db_migration_sqlite_v1.0.6.sql => db_migration_sqlite_v1.0.7.sql} (99%) create mode 100644 src/vault/aws-secrets-manager-provider.js create mode 100644 src/vault/azure-key-vault-provider.js create mode 100644 src/vault/base-vault-provider.js create mode 100644 src/vault/google-secret-manager-provider.js create mode 100644 src/vault/hashicorp-vault-provider.js create mode 100644 src/vault/vault-manager.js diff --git a/package-lock.json b/package-lock.json index 7f5db837..a76d1f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,20 @@ { "name": "@datasance/iofogcontroller", - "version": "3.5.11", + "version": "3.5.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@datasance/iofogcontroller", - "version": "3.5.11", + "version": "3.5.12", "hasInstallScript": true, "license": "EPL-2.0", "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.948.0", + "@azure/identity": "^4.13.0", + "@azure/keyvault-secrets": "^4.10.0", "@datasance/ecn-viewer": "1.2.6", + "@google-cloud/secret-manager": "^6.1.1", "@kubernetes/client-node": "^0.22.3", "@msgpack/msgpack": "^3.1.2", "@opentelemetry/api": "^1.9.0", @@ -83,1098 +87,2529 @@ "standard": "12.0.1" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "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==", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@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==", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, + "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==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, + "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==", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": ">=14.0.0" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "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==", "dependencies": { - "ms": "^2.1.3" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=16.0.0" } }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "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==", + "dependencies": { + "tslib": "^2.6.2" + } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "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==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, + "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==", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, + "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==", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, + "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==", "dependencies": { - "yallist": "^3.0.2" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.948.0.tgz", + "integrity": "sha512-pvGhKhTAPgRN2Nevpj7pywDMh2TuVjA/DCzbybhHyk3cb7Woz3gRW7thvMqaRo/Lnb4SsP6Bkx8P6STb5mXtcA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/credential-provider-node": "3.948.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, + "node_modules/@aws-sdk/client-sso": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", + "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, + "node_modules/@aws-sdk/core": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", + "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.7", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", + "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "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, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", + "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", + "integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/credential-provider-env": "3.947.0", + "@aws-sdk/credential-provider-http": "3.947.0", + "@aws-sdk/credential-provider-login": "3.948.0", + "@aws-sdk/credential-provider-process": "3.947.0", + "@aws-sdk/credential-provider-sso": "3.948.0", + "@aws-sdk/credential-provider-web-identity": "3.948.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", + "integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz", + "integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@aws-sdk/credential-provider-env": "3.947.0", + "@aws-sdk/credential-provider-http": "3.947.0", + "@aws-sdk/credential-provider-ini": "3.948.0", + "@aws-sdk/credential-provider-process": "3.947.0", + "@aws-sdk/credential-provider-sso": "3.948.0", + "@aws-sdk/credential-provider-web-identity": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", + "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", + "integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@aws-sdk/client-sso": "3.948.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/token-providers": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", + "integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", "dependencies": { - "ms": "^2.1.3" + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", + "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=0.1.90" + "node": ">=18.0.0" } }, - "node_modules/@datasance/ecn-viewer": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@datasance/ecn-viewer/-/ecn-viewer-1.2.6.tgz", - "integrity": "sha512-/NV1ll6Vt97P3Fdb8oNDZLHGNNNoUW8zF+VVB0RFEEZpUBZqq5vWx72RyNoTP6Jdr7NqnZUb6PA/1Z9GTN3fvw==" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", + "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@aws-sdk/core": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.7", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", + "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.947.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.948.0", + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.947.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.7", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-retry": "^4.4.14", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.13", + "@smithy/util-defaults-mode-node": "^4.2.16", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "node_modules/@aws-sdk/token-providers": { + "version": "3.948.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", + "integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==", + "dependencies": { + "@aws-sdk/core": "3.947.0", + "@aws-sdk/nested-clients": "3.948.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", - "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", - "dev": true, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", "dependencies": { - "ms": "^2.1.3" + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.947.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", + "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.947.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0" + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "aws-crt": { "optional": true } } }, - "node_modules/@eslint/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", - "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", - "dev": true, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", - "dev": true, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", "dependencies": { - "@types/json-schema": "^7.0.15" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=20.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dependencies": { - "ms": "^2.1.3" + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=20.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://eslint.org/donate" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=20.0.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", - "dev": true, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", - "dev": true, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", "dependencies": { - "@types/json-schema": "^7.0.15" + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@faker-js/faker": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", - "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", - "deprecated": "Please update to a newer version.", - "dev": true - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "optional": true - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.2.tgz", - "integrity": "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==", + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.10.0" + "node": ">=20.0.0" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6" + "node": ">=20.0.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18.18.0" + "node": ">=20.0.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=18.18.0" + "node": ">=20.0.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.0.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "engines": { - "node": ">=18.18" + "node_modules/@azure/keyvault-secrets": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-secrets/-/keyvault-secrets-4.10.0.tgz", + "integrity": "sha512-WvXc3h2hqHL1pMzUU7ANE2RBKoxjK3JQc0YNn6GUFvOWQtf2ZR+sH4/5cZu8zAg62v9qLCduBN7065nHKl+AOA==", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=12" + "node": ">=20.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" + "node_modules/@azure/msal-browser": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.27.0.tgz", + "integrity": "sha512-bZ8Pta6YAbdd0o0PEaL1/geBsPrLEnyY/RDWqvF1PP9RUH8EMLvUMGoZFYS6jSlUan6KZ9IMTLCnwpWWpQRK/w==", + "dependencies": { + "@azure/msal-common": "15.13.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=0.8.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, + "node_modules/@azure/msal-common": { + "version": "15.13.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.3.tgz", + "integrity": "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.8.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "node_modules/@azure/msal-node": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.4.tgz", + "integrity": "sha512-lvuAwsDpPDE/jSuVQOBMpLbXuVuLsPNRwWCyK3/6bPlBk0fGWegqoZ0qjZclMWyQ2JNvIY3vHY7hoFmFmFQcOw==", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@azure/msal-common": "15.13.3", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/@azure/msal-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, "dependencies": { - "minipass": "^7.0.4" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@isaacs/fs-minipass/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" + "yallist": "^3.0.2" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "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/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "engines": { - "node": ">= 10.16.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "node": ">=6.9.0" } }, - "node_modules/@kubernetes/client-node": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.22.3.tgz", - "integrity": "sha512-dG8uah3+HDJLpJEESshLRZlAZ4PgDeV9mZXT0u1g7oy4KMRzdZ7n5g0JEIlL6QhK51/2ztcIqURAnjfjJt6Z+g==", + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, "dependencies": { - "byline": "^5.0.0", - "isomorphic-ws": "^5.0.0", - "js-yaml": "^4.1.0", - "jsonpath-plus": "^10.2.0", - "request": "^2.88.0", - "rfc4648": "^1.3.0", - "stream-buffers": "^3.0.2", - "tar": "^7.0.0", - "tslib": "^2.4.1", - "ws": "^8.18.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, - "optionalDependencies": { - "openid-client": "^6.1.3" - } - }, - "node_modules/@kubernetes/client-node/@cypress/request@3.0.8": {}, - "node_modules/@msgpack/msgpack": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz", - "integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==", "engines": { - "node": ">= 18" + "node": ">=6.9.0" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=6.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "optional": true, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@npmcli/move-file/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@datasance/ecn-viewer": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@datasance/ecn-viewer/-/ecn-viewer-1.2.6.tgz", + "integrity": "sha512-/NV1ll6Vt97P3Fdb8oNDZLHGNNNoUW8zF+VVB0RFEEZpUBZqq5vWx72RyNoTP6Jdr7NqnZUb6PA/1Z9GTN3fvw==" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", + "deprecated": "Please update to a newer version.", + "dev": true + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@google-cloud/secret-manager": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-6.1.1.tgz", + "integrity": "sha512-dwSuxJ9RNmAW46FjK1StiNIeOiSHHQs/XIy4VArJ6bBMR+WsIvR+zhPh2pa40aFa9uTty67j38Rl268TVV62EA==", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.2.tgz", + "integrity": "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, "bin": { - "mkdirp": "bin/cmd.js" + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" }, "engines": { - "node": ">=10" + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@kubernetes/client-node": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.22.3.tgz", + "integrity": "sha512-dG8uah3+HDJLpJEESshLRZlAZ4PgDeV9mZXT0u1g7oy4KMRzdZ7n5g0JEIlL6QhK51/2ztcIqURAnjfjJt6Z+g==", + "dependencies": { + "byline": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^10.2.0", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^7.0.0", + "tslib": "^2.4.1", + "ws": "^8.18.0" + }, + "optionalDependencies": { + "openid-client": "^6.1.3" + } + }, + "node_modules/@kubernetes/client-node/@cypress/request@3.0.8": {}, + "node_modules/@msgpack/msgpack": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz", + "integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz", + "integrity": "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.0.tgz", + "integrity": "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.200.0.tgz", + "integrity": "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A==", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/sdk-logs": "0.200.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.200.0.tgz", + "integrity": "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==", + "dependencies": { + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/sdk-logs": "0.200.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.200.0.tgz", + "integrity": "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA==", + "dependencies": { + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-logs": "0.200.0", + "@opentelemetry/sdk-trace-base": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.200.0.tgz", + "integrity": "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew==", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-metrics": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.200.0.tgz", + "integrity": "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-metrics": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.200.0.tgz", + "integrity": "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-metrics": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.200.0.tgz", + "integrity": "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-metrics": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.200.0.tgz", + "integrity": "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q==", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.200.0.tgz", + "integrity": "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.200.0.tgz", + "integrity": "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.0.tgz", + "integrity": "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.200.0.tgz", + "integrity": "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg==", + "dependencies": { + "@opentelemetry/api-logs": "0.200.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.48.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.48.1.tgz", + "integrity": "sha512-j8NYOf9DRWtchbWor/zA0poI42TpZG9tViIKA0e1lC+6MshTqSJYtgNv8Fn1sx1Wn/TRyp+5OgSXiE4LDfvpEg==", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.200.0.tgz", + "integrity": "sha512-9tqGbCJikhYU68y3k9mi6yWsMyMeCcwoQuHvIXan5VvvPPQ5WIZaV6Mxu/MCVe4swRNoFs8Th+qyj0TZV5ELvw==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/instrumentation": "0.200.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.200.0.tgz", + "integrity": "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-transformer": "0.200.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.200.0.tgz", + "integrity": "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw==", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/otlp-exporter-base": "0.200.0", + "@opentelemetry/otlp-transformer": "0.200.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.200.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.200.0.tgz", + "integrity": "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==", + "dependencies": { + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0", + "@opentelemetry/sdk-logs": "0.200.0", + "@opentelemetry/sdk-metrics": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "dependencies": { + "@opentelemetry/core": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.0.tgz", + "integrity": "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA==", + "dependencies": { + "@opentelemetry/core": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.0.0.tgz", + "integrity": "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA==", + "dependencies": { + "@opentelemetry/core": "2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", "engines": { - "node": ">=8.0.0" + "node": ">=14" } }, - "node_modules/@opentelemetry/api-logs": { + "node_modules/@opentelemetry/sdk-logs": { "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz", - "integrity": "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.200.0.tgz", + "integrity": "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/core": "2.0.0", + "@opentelemetry/resources": "2.0.0" }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.0.tgz", - "integrity": "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==", "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/core": { + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", - "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "dependencies": { + "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.200.0.tgz", - "integrity": "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A==", + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", + "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", "dependencies": { - "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/sdk-logs": "0.200.0" + "@opentelemetry/resources": "2.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.200.0.tgz", - "integrity": "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/sdk-logs": "0.200.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "node_modules/@opentelemetry/sdk-node": { "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.200.0.tgz", - "integrity": "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA==", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.200.0.tgz", + "integrity": "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A==", "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", + "@opentelemetry/exporter-logs-otlp-http": "0.200.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", + "@opentelemetry/exporter-prometheus": "0.200.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", + "@opentelemetry/exporter-trace-otlp-http": "0.200.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", + "@opentelemetry/exporter-zipkin": "2.0.0", + "@opentelemetry/instrumentation": "0.200.0", + "@opentelemetry/propagator-b3": "2.0.0", + "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@opentelemetry/sdk-metrics": "2.0.0", + "@opentelemetry/sdk-trace-base": "2.0.0", + "@opentelemetry/sdk-trace-node": "2.0.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", @@ -1189,28 +2624,23 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.200.0.tgz", - "integrity": "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew==", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.0.tgz", + "integrity": "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==", "dependencies": { - "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", @@ -1225,821 +2655,804 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.200.0.tgz", - "integrity": "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==", + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.0.tgz", + "integrity": "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg==", "dependencies": { + "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@opentelemetry/sdk-trace-base": "2.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@postman/form-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", + "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": "^18.19.0 || >=20.6.0" + "node": ">= 6" + } + }, + "node_modules/@postman/tough-cookie": { + "version": "4.1.3-postman.1", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", + "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "engines": { + "node": ">=6" + } + }, + "node_modules/@postman/tunnel-agent": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz", + "integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.200.0.tgz", - "integrity": "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg==", + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz", + "integrity": "sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@sentry/core": "7.120.4", + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=8" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@sentry/core": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.4.tgz", + "integrity": "sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=8" } }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.200.0.tgz", - "integrity": "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw==", + "node_modules/@sentry/integrations": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz", + "integrity": "sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@sentry/core": "7.120.4", + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4", + "localforage": "^1.8.1" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=8" } }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@sentry/node": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.4.tgz", + "integrity": "sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@sentry-internal/tracing": "7.120.4", + "@sentry/core": "7.120.4", + "@sentry/integrations": "7.120.4", + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=8" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.200.0.tgz", - "integrity": "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q==", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" - }, + "node_modules/@sentry/types": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz", + "integrity": "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==", + "dev": true, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=8" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@sentry/utils": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.4.tgz", + "integrity": "sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@sentry/types": "7.120.4" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=8" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.200.0.tgz", - "integrity": "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "type-detect": "4.0.8" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.200.0.tgz", - "integrity": "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg==", + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", - "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=4" } }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.0.tgz", - "integrity": "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg==", + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.200.0.tgz", - "integrity": "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg==", + "node_modules/@smithy/core": { + "version": "3.18.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", + "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.48.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.48.1.tgz", - "integrity": "sha512-j8NYOf9DRWtchbWor/zA0poI42TpZG9tViIKA0e1lC+6MshTqSJYtgNv8Fn1sx1Wn/TRyp+5OgSXiE4LDfvpEg==", + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.200.0.tgz", - "integrity": "sha512-9tqGbCJikhYU68y3k9mi6yWsMyMeCcwoQuHvIXan5VvvPPQ5WIZaV6Mxu/MCVe4swRNoFs8Th+qyj0TZV5ELvw==", + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/instrumentation": "0.200.0", - "@opentelemetry/semantic-conventions": "^1.29.0", - "forwarded-parse": "2.1.2" + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.200.0.tgz", - "integrity": "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==", + "node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-transformer": "0.200.0" + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.200.0.tgz", - "integrity": "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw==", + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.200.0.tgz", - "integrity": "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==", + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-logs": "0.200.0", - "@opentelemetry/sdk-metrics": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0", - "protobufjs": "^7.3.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/propagator-b3": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.0.tgz", - "integrity": "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA==", + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.14.tgz", + "integrity": "sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==", "dependencies": { - "@opentelemetry/core": "2.0.0" + "@smithy/core": "^3.18.7", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.0.0.tgz", - "integrity": "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA==", + "node_modules/@smithy/middleware-retry": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.14.tgz", + "integrity": "sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==", "dependencies": { - "@opentelemetry/core": "2.0.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/resources": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", - "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "node_modules/@smithy/middleware-serde": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", - "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.28.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.200.0.tgz", - "integrity": "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==", + "node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0" + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", - "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", + "node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-node": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.200.0.tgz", - "integrity": "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A==", + "node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", - "@opentelemetry/exporter-logs-otlp-http": "0.200.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", - "@opentelemetry/exporter-prometheus": "0.200.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", - "@opentelemetry/exporter-trace-otlp-http": "0.200.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", - "@opentelemetry/exporter-zipkin": "2.0.0", - "@opentelemetry/instrumentation": "0.200.0", - "@opentelemetry/propagator-b3": "2.0.0", - "@opentelemetry/propagator-jaeger": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-logs": "0.200.0", - "@opentelemetry/sdk-metrics": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0", - "@opentelemetry/sdk-trace-node": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/types": "^4.9.0" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.0.tgz", - "integrity": "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==", + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.0.tgz", - "integrity": "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg==", + "node_modules/@smithy/smithy-client": { + "version": "4.9.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.10.tgz", + "integrity": "sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==", "dependencies": { - "@opentelemetry/context-async-hooks": "2.0.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@smithy/core": "^3.18.7", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "node": ">=18.0.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", "dependencies": { - "@noble/hashes": "^1.1.5" + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } }, - "node_modules/@postman/form-data": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", - "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", - "dev": true, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 6" + "node": ">=18.0.0" } }, - "node_modules/@postman/tough-cookie": { - "version": "4.1.3-postman.1", - "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", - "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", - "dev": true, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6" + "node": ">=18.0.0" } }, - "node_modules/@postman/tunnel-agent": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz", - "integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==", - "dev": true, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "dependencies": { - "safe-buffer": "^5.0.1" + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "*" + "node": ">=18.0.0" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@sentry-internal/tracing": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz", - "integrity": "sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==", - "dev": true, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.13.tgz", + "integrity": "sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==", "dependencies": { - "@sentry/core": "7.120.4", - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4" + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@sentry/core": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.4.tgz", - "integrity": "sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==", - "dev": true, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.16.tgz", + "integrity": "sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==", "dependencies": { - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4" + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@sentry/integrations": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz", - "integrity": "sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==", - "dev": true, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", "dependencies": { - "@sentry/core": "7.120.4", - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4", - "localforage": "^1.8.1" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@sentry/node": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.4.tgz", - "integrity": "sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==", - "dev": true, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "dependencies": { - "@sentry-internal/tracing": "7.120.4", - "@sentry/core": "7.120.4", - "@sentry/integrations": "7.120.4", - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@sentry/types": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz", - "integrity": "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==", - "dev": true, + "node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@sentry/utils": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.4.tgz", - "integrity": "sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==", - "dev": true, + "node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", "dependencies": { - "@sentry/types": "7.120.4" + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, + "node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", "dependencies": { - "type-detect": "4.0.8" + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", - "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", - "dev": true, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "dependencies": { - "@sinonjs/commons": "^3.0.1" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", - "dev": true, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=4" + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true - }, "node_modules/@testim/chrome-version": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", @@ -2135,6 +3548,19 @@ "@types/node": "*" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -2196,7 +3622,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "optional": true, "engines": { "node": ">= 14" } @@ -2850,6 +4275,11 @@ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2957,11 +4387,30 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -3815,7 +5264,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3996,6 +5444,32 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "devOptional": true }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -4036,6 +5510,17 @@ "capture-stack-trace": "~1.0.0" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -4230,11 +5715,42 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecc-jsbn": { "version": "0.1.2", @@ -4246,6 +5762,14 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", @@ -5301,8 +6825,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/external-editor": { "version": "2.2.0", @@ -5415,6 +6938,23 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -5424,6 +6964,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -5685,6 +7247,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -5885,6 +7458,122 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/gaxios/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/generate-function": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", @@ -6152,10 +7841,153 @@ "gopd": "^1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", + "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/google-gax/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/google-gax/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/google-gax/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "engines": { + "node": ">=14" } }, "node_modules/gopd": { @@ -6175,6 +8007,18 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "devOptional": true }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -6451,7 +8295,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "optional": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -6464,7 +8307,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "optional": true, "dependencies": { "ms": "^2.1.3" }, @@ -6480,8 +8322,7 @@ "node_modules/http-proxy-agent/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "optional": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/http-reasons": { "version": "0.1.0", @@ -6541,7 +8382,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "optional": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -6554,7 +8394,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "optional": true, "dependencies": { "ms": "^2.1.3" }, @@ -6570,8 +8409,7 @@ "node_modules/https-proxy-agent/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "optional": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/humanize-ms": { "version": "1.2.1", @@ -7060,6 +8898,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-elevated": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-elevated/-/is-elevated-3.0.0.tgz", @@ -7135,6 +8987,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", @@ -7421,6 +9290,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is2": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", @@ -7622,7 +9505,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -7823,6 +9705,14 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7916,6 +9806,32 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -7950,6 +9866,16 @@ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jwk-to-pem": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", @@ -7960,6 +9886,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keycloak-connect": { "version": "26.1.1", "resolved": "https://registry.npmjs.org/keycloak-connect/-/keycloak-connect-26.1.1.tgz", @@ -8078,17 +10013,52 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, "node_modules/lodash.isfinite": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9025,6 +10995,42 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-fetch-npm": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz", @@ -9039,6 +11045,14 @@ "node": ">=4" } }, + "node_modules/node-fetch/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/node-forge": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", @@ -9571,6 +11585,23 @@ "node": ">=4" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.1.tgz", @@ -9768,8 +11799,7 @@ "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -9841,7 +11871,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -9855,7 +11884,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -9870,14 +11898,12 @@ "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/path-scurry/node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -10714,6 +12740,17 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -11320,6 +13357,18 @@ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==" }, + "node_modules/retry-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", + "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/rfc4648": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.4.tgz", @@ -11408,6 +13457,17 @@ "node": ">=8.0" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -11912,7 +13972,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11924,7 +13983,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -12987,6 +15045,14 @@ "node": ">= 0.10.0" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dependencies": { + "stubs": "^3.0.0" + } + }, "node_modules/stream-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", @@ -12996,6 +15062,11 @@ "bluebird": "^2.6.2" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -13032,7 +15103,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -13114,7 +15184,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -13151,6 +15220,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, "node_modules/superagent": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", @@ -13549,6 +15634,85 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "node_modules/teeny-request": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", + "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/teeny-request/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/teleport-javascript": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/teleport-javascript/-/teleport-javascript-1.0.0.tgz", @@ -14076,11 +16240,18 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -14255,7 +16426,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -14329,6 +16499,20 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/package.json b/package.json index f3fe3d43..7c80da15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datasance/iofogcontroller", - "version": "3.5.11", + "version": "3.5.12", "description": "ioFog Controller project for Datasance PoT @ datasance.com \\nCopyright (c) 2023 Datasance Teknoloji A.S.", "main": "./src/main.js", "author": "Emirhan Durmus", @@ -55,7 +55,11 @@ "iofog-controller": "src/main.js" }, "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.948.0", + "@azure/keyvault-secrets": "^4.10.0", + "@azure/identity": "^4.13.0", "@datasance/ecn-viewer": "1.2.6", + "@google-cloud/secret-manager": "^6.1.1", "@kubernetes/client-node": "^0.22.3", "@msgpack/msgpack": "^3.1.2", "@opentelemetry/api": "^1.9.0", diff --git a/src/config/controller.yaml b/src/config/controller.yaml index 1e842b7c..d83653d0 100644 --- a/src/config/controller.yaml +++ b/src/config/controller.yaml @@ -2,6 +2,7 @@ app: name: pot-controller # Application name controlPlane: Remote # Control plane type: Remote or Kubernetes or Local + namespace: datasance # Namespace for the application # Server Configuration server: @@ -114,6 +115,34 @@ diagnostics: directory: "diagnostic" # Diagnostics directory +# Vault Configuration (for compliance with NIST, ISO 27001, NATO, IEC 62443) +# When enabled, sensitive data (secrets, certificates, private keys) will be stored +# in the configured vault provider instead of encrypted in the database +# vault: +# enabled: false # Enable/disable vault integration +# provider: hashicorp # Provider: hashicorp, aws, azure, google +# basePath: "pot/$namespace/secrets" # Base path for secrets in vault ($namespace will be replaced with app.namespace) +# # HashiCorp Vault configuration +# hashicorp: +# address: "http://localhost:8200" # Vault server address (can also be set via VAULT_HASHICORP_ADDRESS env var) +# token: "" # Vault token (can also be set via VAULT_HASHICORP_TOKEN env var) +# mount: "secret" # Vault mount point (can also be set via VAULT_HASHICORP_MOUNT env var) +# # AWS Secrets Manager configuration +# aws: +# region: "us-east-1" # AWS region (can also be set via VAULT_AWS_REGION or AWS_REGION env var) +# accessKeyId: "" # AWS access key ID (can also be set via VAULT_AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_ID env var) +# accessKey: "" # AWS secret access key (can also be set via VAULT_AWS_ACCESS_KEY or AWS_SECRET_ACCESS_KEY env var) +# # Azure Key Vault configuration +# azure: +# url: "https://your-vault.vault.azure.net" # Azure Key Vault URL (can also be set via VAULT_AZURE_URL env var) +# tenantId: "" # Azure tenant ID (can also be set via VAULT_AZURE_TENANT_ID or AZURE_TENANT_ID env var) +# clientId: "" # Azure client ID (can also be set via VAULT_AZURE_CLIENT_ID or AZURE_CLIENT_ID env var) +# clientSecret: "" # Azure client secret (can also be set via VAULT_AZURE_CLIENT_SECRET or AZURE_CLIENT_SECRET env var) +# # Google Secret Manager configuration +# google: +# projectId: "" # Google Cloud project ID (required, can also be set via VAULT_GOOGLE_PROJECT_ID env var) +# credentials: "" # Path to service account key file (can also be set via VAULT_GOOGLE_CREDENTIALS or GOOGLE_APPLICATION_CREDENTIALS env var) + # OpenTelemetry Configuration # otel: # enabled: false # true/disable OpenTelemetry diff --git a/src/config/env-mapping.js b/src/config/env-mapping.js index 4a097817..e1b5f4e6 100644 --- a/src/config/env-mapping.js +++ b/src/config/env-mapping.js @@ -2,6 +2,7 @@ module.exports = { // Application Configuration 'APP_NAME': 'app.name', 'CONTROL_PLANE': 'app.controlPlane', + 'CONTROLLER_NAMESPACE': 'app.namespace', // Server Configuration 'SERVER_PORT': 'server.port', @@ -90,6 +91,27 @@ module.exports = { // Diagnostics Configuration 'DIAGNOSTICS_DIRECTORY': 'diagnostics.directory', + // Vault Configuration + 'VAULT_ENABLED': 'vault.enabled', + 'VAULT_PROVIDER': 'vault.provider', + 'VAULT_BASE_PATH': 'vault.basePath', + // HashiCorp Vault + 'VAULT_HASHICORP_ADDRESS': 'vault.hashicorp.address', + 'VAULT_HASHICORP_TOKEN': 'vault.hashicorp.token', + 'VAULT_HASHICORP_MOUNT': 'vault.hashicorp.mount', + // AWS Secrets Manager + 'VAULT_AWS_REGION': 'vault.aws.region', + 'VAULT_AWS_ACCESS_KEY_ID': 'vault.aws.accessKeyId', + 'VAULT_AWS_ACCESS_KEY': 'vault.aws.accessKey', + // Azure Key Vault + 'VAULT_AZURE_URL': 'vault.azure.url', + 'VAULT_AZURE_TENANT_ID': 'vault.azure.tenantId', + 'VAULT_AZURE_CLIENT_ID': 'vault.azure.clientId', + 'VAULT_AZURE_CLIENT_SECRET': 'vault.azure.clientSecret', + // Google Secret Manager + 'VAULT_GOOGLE_PROJECT_ID': 'vault.google.projectId', + 'VAULT_GOOGLE_CREDENTIALS': 'vault.google.credentials', + // OpenTelemetry Configuration 'ENABLE_TELEMETRY': 'otel.enabled', 'OTEL_SERVICE_NAME': 'otel.serviceName', diff --git a/src/data/managers/config-map-manager.js b/src/data/managers/config-map-manager.js index b8da144c..41d9ab43 100644 --- a/src/data/managers/config-map-manager.js +++ b/src/data/managers/config-map-manager.js @@ -8,21 +8,33 @@ class ConfigMapManager extends BaseManager { return ConfigMap } - async createConfigMap (name, immutable, data, transaction) { + async createConfigMap (name, immutable, data, useVault = true, transaction) { return this.create({ name, immutable: immutable, + useVault: useVault, data: data }, transaction) } - async updateConfigMap (name, immutable, data, transaction) { - const encryptedData = await SecretHelper.encryptSecret(data, name) - return this.update( - { name }, - { immutable: immutable, data: encryptedData }, - transaction - ) + async updateConfigMap (name, immutable, data, useVault = null, transaction) { + // Get existing ConfigMap instance to preserve useVault if not explicitly provided + const existing = await this.findOne({ name }, transaction) + if (!existing) { + throw new Error(`ConfigMap ${name} not found`) + } + + // Update instance properties - this will trigger beforeSave hook + existing.immutable = immutable + existing.data = data + // Preserve existing useVault if not explicitly provided, otherwise use new value + existing.useVault = useVault !== null ? useVault : existing.useVault + + // Save the instance - this triggers beforeSave hook which handles encryption/vault + const options = transaction.fakeTransaction ? {} : { transaction: transaction } + await existing.save(options) + + return existing } async getConfigMap (name, transaction) { @@ -42,12 +54,22 @@ class ConfigMapManager extends BaseManager { id: configMap.id, name: configMap.name, immutable: configMap.immutable, + useVault: configMap.useVault, created_at: configMap.created_at, updated_at: configMap.updated_at })) } async deleteConfigMap (name, transaction) { + // Get ConfigMap to check if it's in vault + const configMap = await this.findOne({ name }, transaction) + if (configMap && configMap.useVault) { + // Delete from vault if it was stored there + const vaultManager = require('../../vault/vault-manager') + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(name, 'configmap') + } + } return this.delete({ name }, transaction) } } diff --git a/src/data/managers/iofog-public-key-manager.js b/src/data/managers/iofog-public-key-manager.js index 93004f09..29c8dc35 100644 --- a/src/data/managers/iofog-public-key-manager.js +++ b/src/data/managers/iofog-public-key-manager.js @@ -83,6 +83,11 @@ class FogPublicKeyManager extends BaseManager { } }) } + + // Delete public key by fog UUID + deleteByFogUuid (fogUuid, transaction) { + return this.delete({ iofogUuid: fogUuid }, transaction) + } } const instance = new FogPublicKeyManager() diff --git a/src/data/managers/secret-manager.js b/src/data/managers/secret-manager.js index 6cca804a..02c51205 100644 --- a/src/data/managers/secret-manager.js +++ b/src/data/managers/secret-manager.js @@ -17,11 +17,11 @@ class SecretManager extends BaseManager { }, transaction) } - async updateSecret (name, data, transaction) { - const encryptedData = await SecretHelper.encryptSecret(data, name) + async updateSecret (name, type, data, transaction) { + const encryptedData = await SecretHelper.encryptSecret(data, name, type) return this.update( { name }, - { data: encryptedData }, + { type, data: encryptedData }, transaction ) } diff --git a/src/data/migrations/mysql/db_migration_mysql_v1.0.6.sql b/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql similarity index 99% rename from src/data/migrations/mysql/db_migration_mysql_v1.0.6.sql rename to src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql index 27868646..63c26c32 100644 --- a/src/data/migrations/mysql/db_migration_mysql_v1.0.6.sql +++ b/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql @@ -841,4 +841,6 @@ CREATE INDEX idx_events_method ON Events (method); CREATE INDEX idx_events_event_type ON Events (event_type); CREATE INDEX idx_events_created_at ON Events (created_at); +ALTER TABLE ConfigMaps ADD COLUMN use_vault BOOLEAN DEFAULT true; + COMMIT; \ No newline at end of file diff --git a/src/data/migrations/postgres/db_migration_pg_v1.0.6.sql b/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql similarity index 99% rename from src/data/migrations/postgres/db_migration_pg_v1.0.6.sql rename to src/data/migrations/postgres/db_migration_pg_v1.0.7.sql index 405d70d2..774c714f 100644 --- a/src/data/migrations/postgres/db_migration_pg_v1.0.6.sql +++ b/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql @@ -842,3 +842,4 @@ CREATE INDEX idx_events_method ON "Events" (method); CREATE INDEX idx_events_event_type ON "Events" (event_type); CREATE INDEX idx_events_created_at ON "Events" (created_at); +ALTER TABLE "ConfigMaps" ADD COLUMN use_vault BOOLEAN DEFAULT true; diff --git a/src/data/migrations/sqlite/db_migration_sqlite_v1.0.6.sql b/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql similarity index 99% rename from src/data/migrations/sqlite/db_migration_sqlite_v1.0.6.sql rename to src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql index 8e3ca8e1..ba4d8171 100644 --- a/src/data/migrations/sqlite/db_migration_sqlite_v1.0.6.sql +++ b/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql @@ -828,3 +828,4 @@ CREATE INDEX IF NOT EXISTS idx_events_method ON Events (method); CREATE INDEX IF NOT EXISTS idx_events_event_type ON Events (event_type); CREATE INDEX IF NOT EXISTS idx_events_created_at ON Events (created_at); +ALTER TABLE ConfigMaps ADD COLUMN use_vault BOOLEAN DEFAULT true; \ No newline at end of file diff --git a/src/data/models/configMap.js b/src/data/models/configMap.js index 6f0c6541..63a2c088 100644 --- a/src/data/models/configMap.js +++ b/src/data/models/configMap.js @@ -23,6 +23,12 @@ module.exports = (sequelize, DataTypes) => { field: 'immutable', defaultValue: false }, + useVault: { + type: DataTypes.BOOLEAN, + allowNull: false, + field: 'use_vault', + defaultValue: true + }, data: { type: DataTypes.TEXT, allowNull: false, @@ -30,7 +36,13 @@ module.exports = (sequelize, DataTypes) => { defaultValue: '{}', get () { const rawValue = this.getDataValue('data') - return rawValue ? JSON.parse(rawValue) : {} + if (!rawValue) return {} + if (SecretHelper.isVaultReference(rawValue)) return rawValue + try { + return JSON.parse(rawValue) + } catch (err) { + return rawValue + } }, set (value) { this.setDataValue('data', JSON.stringify(value)) @@ -49,9 +61,22 @@ module.exports = (sequelize, DataTypes) => { hooks: { beforeSave: async (configMap) => { if (configMap.changed('data')) { + // Get useVault value - prioritize getDataValue (for updates), then property, default to true + let useVault = configMap.getDataValue('useVault') + // If getDataValue returns undefined/null, try the property (for new instances) + if (useVault === undefined || useVault === null) { + useVault = configMap.useVault !== undefined && configMap.useVault !== null + ? configMap.useVault + : true + } + // Ensure boolean type + useVault = Boolean(useVault) + const encryptedData = await SecretHelper.encryptSecret( configMap.data, - configMap.name + configMap.name, + 'configmap', + useVault ) configMap.data = encryptedData } @@ -61,7 +86,8 @@ module.exports = (sequelize, DataTypes) => { try { const decryptedData = await SecretHelper.decryptSecret( configMap.data, - configMap.name + configMap.name, + 'configmap' ) configMap.data = decryptedData } catch (error) { diff --git a/src/data/models/secret.js b/src/data/models/secret.js index 9b1447ae..8bfd3ad2 100644 --- a/src/data/models/secret.js +++ b/src/data/models/secret.js @@ -32,7 +32,15 @@ module.exports = (sequelize, DataTypes) => { defaultValue: '{}', get () { const rawValue = this.getDataValue('data') - return rawValue ? JSON.parse(rawValue) : {} + if (!rawValue) return {} + // If value is a vault reference, keep raw so helper can resolve + if (SecretHelper.isVaultReference(rawValue)) return rawValue + try { + return JSON.parse(rawValue) + } catch (err) { + // Fallback: return raw when legacy data is not valid JSON + return rawValue + } }, set (value) { this.setDataValue('data', JSON.stringify(value)) @@ -53,7 +61,8 @@ module.exports = (sequelize, DataTypes) => { if (secret.changed('data')) { const encryptedData = await SecretHelper.encryptSecret( secret.data, - secret.name + secret.name, + secret.type ) secret.data = encryptedData } @@ -63,7 +72,8 @@ module.exports = (sequelize, DataTypes) => { try { const decryptedData = await SecretHelper.decryptSecret( secret.data, - secret.name + secret.name, + secret.type ) secret.data = decryptedData } catch (error) { diff --git a/src/data/providers/database-provider.js b/src/data/providers/database-provider.js index 134b4bf7..6d1ce48c 100644 --- a/src/data/providers/database-provider.js +++ b/src/data/providers/database-provider.js @@ -251,8 +251,8 @@ class DatabaseProvider { // SQLite migration async runMigrationSQLite (dbName) { - const migrationSqlPath = path.resolve(__dirname, '../migrations/sqlite/db_migration_sqlite_v1.0.6.sql') - const migrationVersion = '1.0.6' + const migrationSqlPath = path.resolve(__dirname, '../migrations/sqlite/db_migration_sqlite_v1.0.7.sql') + const migrationVersion = '1.0.7' if (!fs.existsSync(migrationSqlPath)) { logger.error(`Migration file not found: ${migrationSqlPath}`) @@ -324,8 +324,8 @@ class DatabaseProvider { // MySQL migration async runMigrationMySQL (db) { - const migrationSqlPath = path.resolve(__dirname, '../migrations/mysql/db_migration_mysql_v1.0.6.sql') - const migrationVersion = '1.0.6' + const migrationSqlPath = path.resolve(__dirname, '../migrations/mysql/db_migration_mysql_v1.0.7.sql') + const migrationVersion = '1.0.7' if (!fs.existsSync(migrationSqlPath)) { logger.error(`Migration file not found: ${migrationSqlPath}`) @@ -385,8 +385,8 @@ class DatabaseProvider { // PostgreSQL migration async runMigrationPostgres (db) { - const migrationSqlPath = path.resolve(__dirname, '../migrations/postgres/db_migration_pg_v1.0.6.sql') - const migrationVersion = '1.0.6' + const migrationSqlPath = path.resolve(__dirname, '../migrations/postgres/db_migration_pg_v1.0.7.sql') + const migrationVersion = '1.0.7' if (!fs.existsSync(migrationSqlPath)) { logger.error(`Migration file not found: ${migrationSqlPath}`) diff --git a/src/helpers/error-messages.js b/src/helpers/error-messages.js index c12f37b5..9e1b8536 100644 --- a/src/helpers/error-messages.js +++ b/src/helpers/error-messages.js @@ -144,5 +144,6 @@ module.exports = { CONFIGMAP_KEY_NOT_FOUND_IN_VOLUME_MOUNT: 'Configmap key {} not found in configmap {} in volume mount {}', CONFIGMAP_IMMUTABLE: 'Configmap {} is immutable and cannot be updated. If you want to update it, please delete it and create a new configmap.', VOLUME_MOUNT_NOT_FOUND: 'Volume mount with name {} not found', - INVALID_VOLUME_MOUNT_REFERENCE_FOR_VOLUME_MAPPING: 'Invalid volume mount reference for volume mapping: {}' + INVALID_VOLUME_MOUNT_REFERENCE_FOR_VOLUME_MAPPING: 'Invalid volume mount reference for volume mapping: {}', + SECRET_TYPE_MISMATCH: 'Secret type mismatch. Secret {} is of type {} but trying to update with type {}' } diff --git a/src/helpers/secret-helper.js b/src/helpers/secret-helper.js index 6e17595d..1eca13dc 100644 --- a/src/helpers/secret-helper.js +++ b/src/helpers/secret-helper.js @@ -1,4 +1,6 @@ const crypto = require('crypto') +const vaultManager = require('../vault/vault-manager') +const logger = require('../logger') class SecretHelper { constructor () { @@ -8,9 +10,50 @@ class SecretHelper { this.TAG_LENGTH = 16 this.KEY_LENGTH = 32 this.ITERATIONS = 100000 + this.VAULT_REF_PREFIX = 'VAULT_REF:' } - async encryptSecret (secretData, secretName) { + /** + * Store secret data - uses vault if enabled, otherwise uses internal encryption + * @param {Object} secretData - Secret data to store + * @param {string} secretName - Secret name + * @param {string} secretType - Secret type (optional) + * @param {boolean} useVault - For ConfigMaps: whether to use vault (optional, defaults to true if vault enabled) + * @returns {Promise} - Returns encrypted data or vault reference + */ + async encryptSecret (secretData, secretName, secretType = null, useVault = null) { + const isConfigMap = secretType === 'configmap' + + // Determine if vault should be used + let shouldUseVault = false + + if (isConfigMap) { + // For ConfigMaps, check the useVault parameter + if (useVault === false) { + // Explicitly disabled - use internal encryption + shouldUseVault = false + } else if (useVault === true || useVault === null) { + // Explicitly enabled or default (null) - use vault if enabled + shouldUseVault = vaultManager.isEnabled() + } + } else { + // For non-ConfigMaps (Secrets, Agent Auth Keys), always use vault if enabled + shouldUseVault = vaultManager.isEnabled() + } + + // If vault should be used, store in vault + if (shouldUseVault) { + try { + const vaultPath = await vaultManager.store(secretName, secretType, secretData) + // Return vault reference that will be stored in database + return `${this.VAULT_REF_PREFIX}${vaultPath}` + } catch (error) { + logger.error(`Failed to store secret in vault: ${error.message}`) + throw error + } + } + + // Fallback to internal encryption const salt = crypto.randomBytes(this.SALT_LENGTH) const key = await this._deriveKey(secretName, salt) const iv = crypto.randomBytes(this.IV_LENGTH) @@ -23,7 +66,40 @@ class SecretHelper { return Buffer.concat([salt, iv, tag, encrypted]).toString('base64') } - async decryptSecret (encryptedData, secretName) { + /** + * Retrieve secret data - uses vault if reference detected, otherwise uses internal decryption + * @param {string} encryptedData - Encrypted data or vault reference + * @param {string} secretName - Secret name + * @param {string} secretType - Secret type (optional) + * @returns {Promise} - Returns decrypted secret data + */ + async decryptSecret (encryptedData, secretName, secretType = null) { + // Check if this is a vault reference + if (encryptedData && encryptedData.startsWith(this.VAULT_REF_PREFIX)) { + if (!vaultManager.isEnabled()) { + throw new Error('Vault reference found but vault is not enabled') + } + + try { + // The vault path stored in database is the full path from vault + // Extract it and use the provider directly to retrieve + const vaultPath = encryptedData.substring(this.VAULT_REF_PREFIX.length) + const provider = vaultManager.getProvider() + + if (provider) { + // Use the stored vault path directly + return await provider.retrieve(vaultPath) + } else { + // Fallback: reconstruct path from secretName and secretType + return await vaultManager.retrieve(secretName, secretType) + } + } catch (error) { + logger.error(`Failed to retrieve secret from vault: ${error.message}`) + throw error + } + } + + // Fallback to internal decryption const buffer = Buffer.from(encryptedData, 'base64') const salt = buffer.subarray(0, this.SALT_LENGTH) const iv = buffer.subarray(this.SALT_LENGTH, this.SALT_LENGTH + this.IV_LENGTH) @@ -39,6 +115,33 @@ class SecretHelper { return JSON.parse(decrypted.toString('utf8')) } + /** + * Delete secret from vault or database + * @param {string} secretName - Secret name + * @param {string} secretType - Secret type (optional) + * @returns {Promise} + */ + async deleteSecret (secretName, secretType = null) { + if (vaultManager.isEnabled()) { + try { + await vaultManager.delete(secretName, secretType) + } catch (error) { + logger.error(`Failed to delete secret from vault: ${error.message}`) + throw error + } + } + // For internal encryption, deletion is handled by database + } + + /** + * Check if secret is stored in vault + * @param {string} encryptedData - Encrypted data or vault reference + * @returns {boolean} + */ + isVaultReference (encryptedData) { + return encryptedData && encryptedData.startsWith(this.VAULT_REF_PREFIX) + } + async _deriveKey (secretName, salt) { return new Promise((resolve, reject) => { crypto.pbkdf2( diff --git a/src/init.js b/src/init.js index 2da8d026..dc5b76ff 100644 --- a/src/init.js +++ b/src/init.js @@ -18,6 +18,7 @@ require('./config') const logger = require('./logger') const { startTelemetry } = require('./config/telemetry') const db = require('./data/models') +const vaultManager = require('./vault/vault-manager') async function initialize () { try { @@ -29,6 +30,13 @@ async function initialize () { logger.info('Initializing OpenTelemetry...') startTelemetry() + logger.info('Initializing vault integration...') + try { + await vaultManager.initialize() + } catch (error) { + logger.warn(`Vault initialization failed: ${error.message}. Continuing with internal encryption.`) + } + logger.info('Initializing database...') await db.initDB(true) diff --git a/src/schemas/config-map.js b/src/schemas/config-map.js index 2376be9e..38c385a9 100644 --- a/src/schemas/config-map.js +++ b/src/schemas/config-map.js @@ -4,6 +4,7 @@ const configMapCreate = { properties: { name: { type: 'string', minLength: 1, maxLength: 255 }, immutable: { type: 'boolean' }, + useVault: { type: 'boolean' }, data: { type: 'object' } }, required: ['name', 'data'], @@ -16,6 +17,7 @@ const configMapUpdate = { properties: { name: { type: 'string', minLength: 1, maxLength: 255 }, immutable: { type: 'boolean' }, + useVault: { type: 'boolean' }, data: { type: 'object' } }, required: ['data'], @@ -29,6 +31,7 @@ const configMapResponse = { id: { type: 'integer' }, name: { type: 'string' }, immutable: { type: 'boolean' }, + useVault: { type: 'boolean' }, data: { type: 'object' }, created_at: { type: 'string', format: 'date-time' }, updated_at: { type: 'string', format: 'date-time' } @@ -49,6 +52,7 @@ const configMapListResponse = { id: { type: 'integer' }, name: { type: 'string' }, immutable: { type: 'boolean' }, + useVault: { type: 'boolean' }, created_at: { type: 'string', format: 'date-time' }, updated_at: { type: 'string', format: 'date-time' } }, diff --git a/src/schemas/microservice.js b/src/schemas/microservice.js index 670c491d..435e20b5 100644 --- a/src/schemas/microservice.js +++ b/src/schemas/microservice.js @@ -238,7 +238,7 @@ const volumeMappings = { 'hostDestination': { 'type': 'string' }, 'containerDestination': { 'type': 'string' }, 'accessMode': { 'type': 'string' }, - 'type': { 'enum': ['volume', 'bind'] } + 'type': { 'enum': ['volume', 'bind', 'volumeMount'] } }, 'required': ['hostDestination', 'containerDestination', 'accessMode'], 'additionalProperties': true diff --git a/src/services/agent-service.js b/src/services/agent-service.js index 0fbd0705..3bb1f822 100644 --- a/src/services/agent-service.js +++ b/src/services/agent-service.js @@ -46,6 +46,7 @@ const EdgeResourceService = require('./edge-resource-service') const constants = require('../helpers/constants') const SecretManager = require('../data/managers/secret-manager') const ConfigMapManager = require('../data/managers/config-map-manager') + const IncomingForm = formidable.IncomingForm const CHANGE_TRACKING_DEFAULT = {} const CHANGE_TRACKING_KEYS = ['config', 'version', 'reboot', 'deleteNode', 'microserviceList', 'microserviceConfig', 'routing', 'registries', 'tunnel', 'diagnostics', 'isImageSnapshot', 'prune', 'routerChanged', 'linkedEdgeResources', 'volumeMounts', 'execSessions'] @@ -121,6 +122,9 @@ const agentDeprovision = async function (deprovisionData, fog, transaction) { ) await _invalidateFogNode(fog, transaction) + + // Delete the public key + await FogKeyService.deletePublicKey(fog.uuid, transaction) } const _invalidateFogNode = async function (fog, transaction) { @@ -748,19 +752,22 @@ const getAgentLinkedVolumeMounts = async function (fog, transaction) { for (const resource of resources) { const resourceObject = resource.toJSON() let data = {} + let type = null if (resourceObject.configMapName) { // Handle ConfigMap + type = 'configMap' const configMap = await ConfigMapManager.getConfigMap(resourceObject.configMapName, transaction) if (configMap) { // For configmaps, we need to base64 encode all values data = Object.entries(configMap.data).reduce((acc, [key, value]) => { - acc[key] = Buffer.from(value).toString('base64') + acc[key] = Buffer.from(String(value)).toString('base64') return acc }, {}) } } else if (resourceObject.secretName) { // Handle Secret + type = 'secret' const secret = await SecretManager.getSecret(resourceObject.secretName, transaction) if (secret) { if (secret.type === 'tls') { @@ -769,7 +776,7 @@ const getAgentLinkedVolumeMounts = async function (fog, transaction) { } else { // For Opaque secrets, we need to base64 encode all values data = Object.entries(secret.data).reduce((acc, [key, value]) => { - acc[key] = Buffer.from(value).toString('base64') + acc[key] = Buffer.from(String(value)).toString('base64') return acc }, {}) } @@ -781,6 +788,7 @@ const getAgentLinkedVolumeMounts = async function (fog, transaction) { uuid: resourceObject.uuid, name: resourceObject.name, version: resourceObject.version, + type: type, data: data } volumeMounts.push(responseObject) diff --git a/src/services/config-map-service.js b/src/services/config-map-service.js index a1aa48c4..25fb1859 100644 --- a/src/services/config-map-service.js +++ b/src/services/config-map-service.js @@ -34,11 +34,22 @@ async function createConfigMapEndpoint (configMapData, transaction) { throw new Errors.ConflictError(AppHelper.formatMessage(ErrorMessages.CONFIGMAP_ALREADY_EXISTS, configMapData.name)) } - const configMap = await ConfigMapManager.createConfigMap(configMapData.name, configMapData.immutable, configMapData.data, transaction) + // Extract useVault, default to true if not provided (backward compatible) + const useVault = configMapData.useVault !== undefined ? configMapData.useVault : true + + const configMap = await ConfigMapManager.createConfigMap( + configMapData.name, + configMapData.immutable, + configMapData.data, + useVault, + transaction + ) + return { id: configMap.id, name: configMap.name, immutable: configMap.immutable, + useVault: configMap.useVault, created_at: configMap.created_at, updated_at: configMap.updated_at } @@ -59,12 +70,25 @@ async function updateConfigMapEndpoint (configMapName, configMapData, transactio throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.CONFIGMAP_IMMUTABLE, configMapName)) } - const configMap = await ConfigMapManager.updateConfigMap(configMapName, configMapData.immutable, configMapData.data, transaction) + // Extract useVault if provided, otherwise keep existing value (null = don't change) + const useVault = configMapData.useVault !== undefined ? configMapData.useVault : null + + const configMap = await ConfigMapManager.updateConfigMap( + configMapName, + configMapData.immutable, + configMapData.data, + useVault, + transaction + ) + await _updateChangeTrackingForFogs(configMapName, transaction) await _updateMicroservicesUsingConfigMap(configMapName, transaction) + return { id: configMap.id, name: configMap.name, + immutable: configMap.immutable, + useVault: configMap.useVault, created_at: configMap.created_at, updated_at: configMap.updated_at } @@ -81,6 +105,7 @@ async function getConfigMapEndpoint (configMapName, transaction) { name: configMap.name, data: configMap.data, immutable: configMap.immutable, + useVault: configMap.useVault, created_at: configMap.created_at, updated_at: configMap.updated_at } @@ -93,6 +118,7 @@ async function listConfigMapsEndpoint (transaction) { id: configMap.id, name: configMap.name, immutable: configMap.immutable, + useVault: configMap.useVault, created_at: configMap.created_at, updated_at: configMap.updated_at })) @@ -107,6 +133,7 @@ async function deleteConfigMapEndpoint (configMapName, transaction) { await ConfigMapManager.deleteConfigMap(configMapName, transaction) await _deleteVolumeMountsUsingConfigMap(configMapName, transaction) + // Vault deletion is handled by ConfigMapManager.deleteConfigMap() return {} } diff --git a/src/services/iofog-key-service.js b/src/services/iofog-key-service.js index d34927c9..134f5556 100644 --- a/src/services/iofog-key-service.js +++ b/src/services/iofog-key-service.js @@ -16,6 +16,7 @@ const FogPublicKeyManager = require('../data/managers/iofog-public-key-manager') const FogUsedTokenManager = require('../data/managers/fog-used-token-manager') const SecretHelper = require('../helpers/secret-helper') const jose = require('jose') +const vaultManager = require('../vault/vault-manager') /** * Generate Ed25519 key pair and return as JWK strings @@ -47,10 +48,12 @@ const generateKeyPair = async function (transaction) { * @returns {Promise} Promise resolving to the stored public key */ const storePublicKey = async function (fogUuid, publicKey, transaction) { - // Encrypt the public key using SecretHelper for better security and database compatibility - const encryptedPublicKey = await SecretHelper.encryptSecret(publicKey, fogUuid) + // Encrypt the public key using SecretHelper (will use vault if enabled) + // SecretHelper will automatically route to vault or internal encryption + // Wrap string so KV backends (e.g., HashiCorp) get an object payload + const encryptedPublicKey = await SecretHelper.encryptSecret({ value: publicKey }, fogUuid, 'agent-auth-key') - // Store the encrypted public key + // Store the encrypted public key or vault reference return FogPublicKeyManager.updateOrCreate(fogUuid, encryptedPublicKey, transaction) } @@ -68,10 +71,24 @@ const getPublicKey = async function (fogUuid, transaction) { return null } - // Decrypt the public key using SecretHelper for better security and database compatibility - return SecretHelper.decryptSecret(fogPublicKey.publicKey, fogUuid) + // Decrypt the public key using SecretHelper (will use vault if enabled) + const decrypted = await SecretHelper.decryptSecret(fogPublicKey.publicKey, fogUuid, 'agent-auth-key') + return decrypted && decrypted.value ? decrypted.value : decrypted } +/** + * Delete the public key for a fog node + * @param {string} fogUuid - UUID of the fog node + * @param {Object} transaction - Sequelize transaction + * @returns {Promise} Promise resolving to the deleted public key + */ +const deletePublicKey = async function (fogUuid, transaction) { + await FogPublicKeyManager.deleteByFogUuid(fogUuid, transaction) + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(fogUuid, 'agent-auth-key') + } + return {} +} /** * Verify a JWT signed by a fog node * @param {string} token - JWT token @@ -125,6 +142,7 @@ module.exports = { generateKeyPair, storePublicKey, getPublicKey, + deletePublicKey, verifyJWT, all } diff --git a/src/services/iofog-service.js b/src/services/iofog-service.js index 5fe1e23f..285e4c37 100644 --- a/src/services/iofog-service.js +++ b/src/services/iofog-service.js @@ -17,6 +17,7 @@ const TransactionDecorator = require('../decorators/transaction-decorator') const AppHelper = require('../helpers/app-helper') const FogManager = require('../data/managers/iofog-manager') const FogProvisionKeyManager = require('../data/managers/iofog-provision-key-manager') +const FogKeyService = require('./iofog-key-service') const FogVersionCommandManager = require('../data/managers/iofog-version-command-manager') const ChangeTrackingService = require('./change-tracking-service') const Errors = require('../helpers/errors') @@ -45,6 +46,9 @@ const logger = require('../logger') const ServiceManager = require('../data/managers/service-manager') const FogStates = require('../enums/fog-state') const SecretManager = require('../data/managers/secret-manager') +const vaultManager = require('../vault/vault-manager') +const SecretHelper = require('../helpers/secret-helper') +const FogPublicKeyManager = require('../data/managers/iofog-public-key-manager') const SITE_CA_CERT = 'pot-site-ca' const DEFAULT_ROUTER_LOCAL_CA = 'default-router-local-ca' @@ -1095,9 +1099,17 @@ async function _processDeleteCommand (fog, transaction) { for (const secretName of secretNames) { const secret = await SecretManager.findOne({ name: secretName }, transaction) if (secret) { + // Remove secret from external vault if configured + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(secretName, secret.type) + } await SecretManager.delete({ name: secretName }, transaction) } } + const fogPublicKey = await FogPublicKeyManager.findByFogUuid(fog.uuid, transaction) + if (fogPublicKey) { + await FogKeyService.deletePublicKey(fog.uuid, transaction) + } await FogManager.delete({ uuid: fog.uuid }, transaction) } diff --git a/src/services/microservices-service.js b/src/services/microservices-service.js index 90b6d779..aaa3d967 100644 --- a/src/services/microservices-service.js +++ b/src/services/microservices-service.js @@ -464,6 +464,11 @@ function _validateVolumeMappings (volumeMappings) { throw new Errors.InvalidArgumentError('hostDestination includes invalid characters for a local volume name, only ' + '"[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use type: bind') } + if (mapping.type === 'volumeMount') { + if (!mapping.hostDestination || mapping.hostDestination === '') { + throw new Errors.ValidationError('hostDestination is required when type is volumeMount') + } + } } } } @@ -505,24 +510,32 @@ function _validateKeyPath (data, keyPath, resourceName, resourceType, volumeMoun } } -async function _validateVolumeMountReference (hostDestination, fogUuid, transaction) { +/** + * Validates a volume mount reference when type is 'volumeMount' + * @param {string} hostDestination - Volume mount reference in format: / + * @param {string} type - Volume mapping type ('volume', 'bind', or 'volumeMount') + * @param {string} fogUuid - UUID of the fog node + * @param {object} transaction - Database transaction + * @returns {Promise} + */ +async function _validateVolumeMountReference (hostDestination, type, fogUuid, transaction) { if (!hostDestination || typeof hostDestination !== 'string') { return // No validation needed if hostDestination is empty or not a string } - // Check if hostDestination starts with $VolumeMount/ - if (!hostDestination.startsWith('$VolumeMount/')) { + // Check if type is volumeMount - only validate when explicitly using volumeMount type + if (type !== 'volumeMount') { return // Not a volume mount reference, skip validation } - // Parse the volume mount reference: $VolumeMount// - const withoutPrefix = hostDestination.substring('$VolumeMount/'.length) - if (!withoutPrefix || withoutPrefix === '') { + // Parse the volume mount reference: / + // Format: "my-volume-mount" or "my-volume-mount/config/app.conf" + if (!hostDestination || hostDestination === '') { throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.INVALID_VOLUME_MOUNT_REFERENCE_FOR_VOLUME_MAPPING, 'Volume mount reference must include a volume mount name')) } // Split by '/' to separate volume mount name and optional key path - const parts = withoutPrefix.split('/') + const parts = hostDestination.split('/') const volumeMountName = parts[0] const keyPath = parts.length > 1 ? parts.slice(1).join('/') : null @@ -1700,6 +1713,12 @@ async function createVolumeMappingEndPoint (microserviceUuid, volumeMappingData, _validateVolumeMappings([volumeMappingData]) + // Validate volume mount references before creating mapping + // When type is 'volumeMount', validates that the volume mount exists and is linked to the fog node + if (volumeMappingData.hostDestination && microservice.iofogUuid) { + await _validateVolumeMountReference(volumeMappingData.hostDestination, type, microservice.iofogUuid, transaction) + } + const volumeMappingObj = { microserviceUuid: microserviceUuid, hostDestination: volumeMappingData.hostDestination, @@ -1737,6 +1756,12 @@ async function createSystemVolumeMappingEndPoint (microserviceUuid, volumeMappin _validateVolumeMappings([volumeMappingData]) + // Validate volume mount references before creating mapping + // When type is 'volumeMount', validates that the volume mount exists and is linked to the fog node + if (volumeMappingData.hostDestination && microservice.iofogUuid) { + await _validateVolumeMountReference(volumeMappingData.hostDestination, type, microservice.iofogUuid, transaction) + } + const volumeMappingObj = { microserviceUuid: microserviceUuid, hostDestination: volumeMappingData.hostDestination, @@ -1985,10 +2010,12 @@ async function _createMicroserviceImages (microservice, images, transaction) { async function _createVolumeMappings (microservice, volumeMappings, transaction) { // Validate volume mount references before creating mappings + // When type is 'volumeMount', validates that the volume mount exists and is linked to the fog node if (volumeMappings && microservice.iofogUuid) { for (const volumeMapping of volumeMappings) { if (volumeMapping.hostDestination) { - await _validateVolumeMountReference(volumeMapping.hostDestination, microservice.iofogUuid, transaction) + const type = volumeMapping.type || VOLUME_MAPPING_DEFAULT + await _validateVolumeMountReference(volumeMapping.hostDestination, type, microservice.iofogUuid, transaction) } } } @@ -2013,10 +2040,12 @@ async function _updateVolumeMappings (volumeMappings, microserviceUuid, transact } // Validate volume mount references before updating mappings + // When type is 'volumeMount', validates that the volume mount exists and is linked to the fog node if (volumeMappings && microservice.iofogUuid) { for (const volumeMapping of volumeMappings) { if (volumeMapping.hostDestination) { - await _validateVolumeMountReference(volumeMapping.hostDestination, microservice.iofogUuid, transaction) + const type = volumeMapping.type || VOLUME_MAPPING_DEFAULT + await _validateVolumeMountReference(volumeMapping.hostDestination, type, microservice.iofogUuid, transaction) } } } diff --git a/src/services/router-connection-service.js b/src/services/router-connection-service.js index 38d110c3..2fff2249 100644 --- a/src/services/router-connection-service.js +++ b/src/services/router-connection-service.js @@ -153,7 +153,7 @@ class RouterConnectionService { let host = router.host && router.host.trim().length > 0 ? router.host.trim() : '' if (this._isKubernetes()) { - const namespace = process.env.CONTROLLER_NAMESPACE + const namespace = process.env.CONTROLLER_NAMESPACE || config.get('app.namespace') if (namespace && namespace.trim().length > 0) { host = `${DEFAULT_ROUTER_SERVICE}.${namespace}.svc.cluster.local` } else if (!host) { diff --git a/src/services/router-service.js b/src/services/router-service.js index bcce7917..5daada35 100644 --- a/src/services/router-service.js +++ b/src/services/router-service.js @@ -32,9 +32,10 @@ const constants = require('../helpers/constants') const MicroserviceEnvManager = require('../data/managers/microservice-env-manager') const SecretManager = require('../data/managers/secret-manager') const FogManager = require('../data/managers/iofog-manager') +const config = require('../config') const SITE_CONFIG_VERSION = 'pot' -const SITE_CONFIG_NAMESPACE = 'datasance' +const SITE_CONFIG_NAMESPACE = process.env.CONTROLLER_NAMESPACE || config.get('app.namespace') async function validateAndReturnUpstreamRouters (upstreamRouterIds, isSystemFog, defaultRouter, transaction) { if (!upstreamRouterIds) { @@ -399,10 +400,6 @@ async function _getRouterMicroserviceConfig (isEdge, uuid, messagingPort, interR platform = 'podman' } - let namespace = SITE_CONFIG_NAMESPACE - if (process.env.CONTROLLER_NAMESPACE) { - namespace = process.env.CONTROLLER_NAMESPACE - } const config = { addresses: { mc: { @@ -429,7 +426,7 @@ async function _getRouterMicroserviceConfig (isEdge, uuid, messagingPort, interR }, siteConfig: { name: uuid, - namespace: namespace, + namespace: SITE_CONFIG_NAMESPACE, platform: platform, version: SITE_CONFIG_VERSION }, diff --git a/src/services/secret-service.js b/src/services/secret-service.js index c788d255..312758e6 100644 --- a/src/services/secret-service.js +++ b/src/services/secret-service.js @@ -23,6 +23,8 @@ const Validator = require('../schemas/index') const VolumeMountService = require('./volume-mount-service') const VolumeMountingManager = require('../data/managers/volume-mounting-manager') const CertificateManager = require('../data/managers/certificate-manager') +const SecretHelper = require('../helpers/secret-helper') +const vaultManager = require('../vault/vault-manager') function validateBase64 (value) { try { @@ -82,9 +84,13 @@ async function updateSecretEndpoint (secretName, secretData, transaction) { throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.SECRET_NOT_FOUND, secretName)) } + if (existingSecret.type !== secretData.type) { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SECRET_TYPE_MISMATCH, secretName, existingSecret.type, secretData.type)) + } + validateSecretData(existingSecret.type, secretData.data) - const secret = await SecretManager.updateSecret(secretName, secretData.data, transaction) + const secret = await SecretManager.updateSecret(secretName, secretData.type, secretData.data, transaction) await _updateChangeTrackingForFogs(secretName, transaction) await _updateMicroservicesUsingSecret(secretName, transaction) return { @@ -141,15 +147,37 @@ async function deleteSecretEndpoint (secretName, transaction) { throw new Errors.ValidationError(`Cannot delete CA that has signed certificates. Please delete the following certificates first: ${signedCerts.map(cert => cert.name).join(', ')}`) } await CertificateManager.deleteCertificate(certificate.name, transaction) + await SecretManager.deleteSecret(secretName, transaction) + // Remove secret from external vault if configured + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(secretName, existingSecret.type) + } await _deleteVolumeMountsUsingSecret(secretName, transaction) } else { await CertificateManager.deleteCertificate(certificate.name, transaction) await _deleteVolumeMountsUsingSecret(secretName, transaction) + await SecretManager.deleteSecret(secretName, transaction) + // Remove secret from external vault if configured + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(secretName, existingSecret.type) + } + } + } else { + // Delete secret from database and external vault + await SecretManager.deleteSecret(secretName, transaction) + await _deleteVolumeMountsUsingSecret(secretName, transaction) + // Remove secret from external vault if configured + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(secretName, existingSecret.type) } } } else { await SecretManager.deleteSecret(secretName, transaction) await _deleteVolumeMountsUsingSecret(secretName, transaction) + // Remove secret from external vault if configured + if (vaultManager.isEnabled()) { + await SecretHelper.deleteSecret(secretName, existingSecret.type) + } } return {} } diff --git a/src/services/yaml-parser-service.js b/src/services/yaml-parser-service.js index 084f2f3e..1fd3de44 100644 --- a/src/services/yaml-parser-service.js +++ b/src/services/yaml-parser-service.js @@ -150,18 +150,28 @@ async function parseConfigMapFile (fileContent, options = {}) { throw new Errors.ValidationError(`ConfigMap name in YAML (${doc.metadata.name}) doesn't match endpoint path (${options.configMapName})`) } - // For updates, we only need the data - return { - data: doc.data + // For updates, return data and useVault if provided + const result = { + data: doc.data, + immutable: doc.spec.immutable + } + if (doc.spec && doc.spec.useVault !== undefined) { + result.useVault = doc.spec.useVault } + return result } // For creates, return full object - return { + const result = { name: lget(doc, 'metadata.name', undefined), data: doc.data, immutable: doc.spec.immutable } + // Include useVault if specified in YAML + if (doc.spec && doc.spec.useVault !== undefined) { + result.useVault = doc.spec.useVault + } + return result } catch (error) { if (error instanceof Errors.ValidationError) { throw error diff --git a/src/vault/aws-secrets-manager-provider.js b/src/vault/aws-secrets-manager-provider.js new file mode 100644 index 00000000..5faa3a71 --- /dev/null +++ b/src/vault/aws-secrets-manager-provider.js @@ -0,0 +1,191 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseVaultProvider = require('./base-vault-provider') +const logger = require('../logger') + +class AWSSecretsManagerProvider extends BaseVaultProvider { + constructor () { + super() + this.client = null + } + + getName () { + return 'aws-secrets-manager' + } + + async initialize (config) { + // Call parent initialize to set this.config + await super.initialize(config) + + // AWS SDK v3 uses dynamic imports + try { + const { SecretsManagerClient, GetSecretValueCommand, CreateSecretCommand, DeleteSecretCommand, ListSecretsCommand, PutSecretValueCommand } = require('@aws-sdk/client-secrets-manager') + + // Build credentials object if accessKeyId is provided + let credentials + if (config.accessKeyId && config.accessKey) { + credentials = { + accessKeyId: config.accessKeyId, + secretAccessKey: config.accessKey + } + } + + this.client = new SecretsManagerClient({ + region: config.region, + credentials: credentials + }) + + this.GetSecretValueCommand = GetSecretValueCommand + this.CreateSecretCommand = CreateSecretCommand + this.DeleteSecretCommand = DeleteSecretCommand + this.ListSecretsCommand = ListSecretsCommand + this.PutSecretValueCommand = PutSecretValueCommand + + // Store reference for use in store method + this.PutSecretValueCommandClass = PutSecretValueCommand + + // Test connection + await this.testConnection() + } catch (error) { + // Provide more specific error messages + if (error.code === 'MODULE_NOT_FOUND' || error.message.includes('Cannot find module')) { + throw new Error(`Failed to initialize AWS Secrets Manager: @aws-sdk/client-secrets-manager package is not installed. Please run: npm install @aws-sdk/client-secrets-manager`) + } + if (error.code === 'ENOTFOUND' || error.message.includes('getaddrinfo ENOTFOUND')) { + throw new Error(`Failed to connect to AWS Secrets Manager: Invalid region "${config.region}" or network connectivity issue. Please verify the AWS region is correct (e.g., us-east-1, eu-west-1).`) + } + if (error.name === 'CredentialsProviderError' || error.message.includes('credentials')) { + throw new Error(`Failed to initialize AWS Secrets Manager: Invalid credentials. Please verify AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are correct.`) + } + throw new Error(`Failed to initialize AWS Secrets Manager: ${error.message}`) + } + } + + async store (path, data) { + const secretName = this.buildPath(path) + const command = new this.CreateSecretCommand({ + Name: secretName, + SecretString: JSON.stringify(data), + Description: `Datasance PoT controller secret: ${path}` + }) + + try { + const response = await this.client.send(command) + logger.debug(`AWS Secrets Manager store response: ${JSON.stringify(response)}`) + return secretName + } catch (error) { + // If secret already exists, use PutSecretValueCommand to update + if (error.name === 'ResourceExistsException') { + const updateCommand = new this.PutSecretValueCommandClass({ + SecretId: secretName, + SecretString: JSON.stringify(data) + }) + const response = await this.client.send(updateCommand) + logger.debug(`AWS Secrets Manager update response: ${JSON.stringify(response)}`) + return secretName + } + throw error + } + } + + async retrieve (path) { + const secretName = this.buildPath(path) + const command = new this.GetSecretValueCommand({ + SecretId: secretName + }) + + try { + const response = await this.client.send(command) + if (response.SecretString) { + return JSON.parse(response.SecretString) + } + if (response.SecretBinary) { + return JSON.parse(Buffer.from(response.SecretBinary, 'base64').toString()) + } + throw new Error('Secret has no string or binary value') + } catch (error) { + if (error.name === 'ResourceNotFoundException') { + throw new Error(`Secret not found: ${secretName}`) + } + throw error + } + } + + async delete (path) { + const secretName = this.buildPath(path) + const command = new this.DeleteSecretCommand({ + SecretId: secretName, + ForceDeleteWithoutRecovery: true + }) + + try { + const response = await this.client.send(command) + logger.debug(`AWS Secrets Manager delete response: ${JSON.stringify(response)}`) + } catch (error) { + if (error.name !== 'ResourceNotFoundException') { + throw error + } + } + } + + async exists (path) { + try { + await this.retrieve(path) + return true + } catch (error) { + if (error.message.includes('not found') || error.name === 'ResourceNotFoundException') { + return false + } + throw error + } + } + + async list (path) { + const prefix = this.buildPath(path) + const command = new this.ListSecretsCommand({ + Filters: [ + { + Key: 'name', + Values: [prefix] + } + ] + }) + + try { + const response = await this.client.send(command) + return (response.SecretList || []).map(secret => secret.Name) + } catch (error) { + return [] + } + } + + async testConnection () { + try { + const command = new this.ListSecretsCommand({ MaxResults: 1 }) + await this.client.send(command) + return true + } catch (error) { + throw new Error(`Failed to connect to AWS Secrets Manager: ${error.message}`) + } + } + + getBasePath () { + if (this.config && this.config.basePath && typeof this.config.basePath === 'string') { + return this.config.basePath + } + return 'pot-controller/secrets' + } +} + +module.exports = AWSSecretsManagerProvider diff --git a/src/vault/azure-key-vault-provider.js b/src/vault/azure-key-vault-provider.js new file mode 100644 index 00000000..7158ba9a --- /dev/null +++ b/src/vault/azure-key-vault-provider.js @@ -0,0 +1,166 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseVaultProvider = require('./base-vault-provider') + +class AzureKeyVaultProvider extends BaseVaultProvider { + constructor () { + super() + this.client = null + } + + getName () { + return 'azure-key-vault' + } + + async initialize (config) { + // Call parent initialize to set this.config + await super.initialize(config) + + const vaultUrl = config.url + if (!vaultUrl) { + throw new Error('Azure Key Vault URL is required') + } + + try { + const { SecretClient } = require('@azure/keyvault-secrets') + const { DefaultAzureCredential, ClientSecretCredential } = require('@azure/identity') + + let credential + // Check if credentials are provided in config + if (config.tenantId && config.clientId && config.clientSecret) { + credential = new ClientSecretCredential( + config.tenantId, + config.clientId, + config.clientSecret + ) + } else { + credential = new DefaultAzureCredential() + } + + this.client = new SecretClient(vaultUrl, credential) + + // Test connection + await this.testConnection() + } catch (error) { + throw new Error(`Failed to initialize Azure Key Vault: ${error.message}. Make sure @azure/keyvault-secrets and @azure/identity are installed.`) + } + } + + async store (path, data) { + const secretName = this._sanitizeSecretName(this.buildPath(path)) + const secretValue = JSON.stringify(data) + + try { + await this.client.setSecret(secretName, secretValue) + return secretName + } catch (error) { + throw new Error(`Failed to store secret in Azure Key Vault: ${error.message}`) + } + } + + async retrieve (path) { + const secretName = this._sanitizeSecretName(this.buildPath(path)) + + try { + const secret = await this.client.getSecret(secretName) + if (secret.value) { + return JSON.parse(secret.value) + } + throw new Error('Secret has no value') + } catch (error) { + if (error.statusCode === 404) { + throw new Error(`Secret not found: ${secretName}`) + } + throw error + } + } + + async delete (path) { + const secretName = this._sanitizeSecretName(this.buildPath(path)) + + try { + const poller = await this.client.beginDeleteSecret(secretName) + await poller.pollUntilDone() + // Purge to fully remove (align with immediate delete semantics) + try { + await this.client.purgeDeletedSecret(secretName) + } catch (purgeError) { + // Ignore if purge not supported or already gone + if (purgeError.statusCode && purgeError.statusCode !== 404 && purgeError.statusCode !== 403) { + throw purgeError + } + } + } catch (error) { + if (error.statusCode !== 404) { + throw error + } + } + } + + async exists (path) { + try { + await this.retrieve(path) + return true + } catch (error) { + if (error.message.includes('not found') || error.statusCode === 404) { + return false + } + throw error + } + } + + async list (path) { + const prefix = this._sanitizeSecretName(this.buildPath(path)) + + try { + const secrets = [] + for await (const secretProperties of this.client.listPropertiesOfSecrets()) { + if (secretProperties.name.startsWith(prefix)) { + secrets.push(secretProperties.name) + } + } + return secrets + } catch (error) { + return [] + } + } + + async testConnection () { + try { + // Try to list secrets to test connection + const iterator = this.client.listPropertiesOfSecrets() + await iterator.next() + return true + } catch (error) { + throw new Error(`Failed to connect to Azure Key Vault: ${error.message}`) + } + } + + /** + * Sanitize secret name for Azure Key Vault + * Azure Key Vault secret names can only contain alphanumeric characters and hyphens + */ + _sanitizeSecretName (name) { + return name.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase() + } + + getBasePath () { + if (this.config && this.config.basePath && typeof this.config.basePath === 'string') { + return this.config.basePath + } + return 'pot-controller/secrets' + } +} + +module.exports = AzureKeyVaultProvider diff --git a/src/vault/base-vault-provider.js b/src/vault/base-vault-provider.js new file mode 100644 index 00000000..f509c2c1 --- /dev/null +++ b/src/vault/base-vault-provider.js @@ -0,0 +1,148 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +/** + * Base class for vault providers + * All vault providers must implement this interface + */ +class BaseVaultProvider { + constructor () { + this.config = null + } + + /** + * Initialize the vault provider with configuration + * This method should be called by child classes via super.initialize(config) + * @param {Object} config - Provider-specific configuration + * @returns {Promise} + */ + async initialize (config) { + if (!config || typeof config !== 'object') { + throw new Error(`${this.constructor.name}: initialize() requires a valid config object`) + } + this.config = config + // Child classes should override this method and call super.initialize(config) first + } + + /** + * Store a secret in the vault + * @param {string} path - Secret path/identifier + * @param {Object} data - Secret data to store + * @returns {Promise} - Returns vault reference/identifier + */ + async store (path, data) { + throw new Error(`${this.constructor.name}: store() must be implemented by vault provider`) + } + + /** + * Retrieve a secret from the vault + * @param {string} path - Secret path/identifier + * @returns {Promise} - Returns secret data + */ + async retrieve (path) { + throw new Error(`${this.constructor.name}: retrieve() must be implemented by vault provider`) + } + + /** + * Delete a secret from the vault + * @param {string} path - Secret path/identifier + * @returns {Promise} + */ + async delete (path) { + throw new Error(`${this.constructor.name}: delete() must be implemented by vault provider`) + } + + /** + * Check if a secret exists in the vault + * @param {string} path - Secret path/identifier + * @returns {Promise} + */ + async exists (path) { + throw new Error(`${this.constructor.name}: exists() must be implemented by vault provider`) + } + + /** + * List all secrets under a given path + * @param {string} path - Base path to list from + * @returns {Promise} - Returns array of secret paths + */ + async list (path) { + throw new Error(`${this.constructor.name}: list() must be implemented by vault provider`) + } + + /** + * Test the connection to the vault + * @returns {Promise} + */ + async testConnection () { + throw new Error(`${this.constructor.name}: testConnection() must be implemented by vault provider`) + } + + /** + * Get provider name + * @returns {string} + */ + getName () { + throw new Error(`${this.constructor.name}: getName() must be implemented by vault provider`) + } + + /** + * Normalize a path by removing leading/trailing slashes and multiple slashes + * @param {string} path - Path to normalize + * @returns {string} - Normalized path + * @private + */ + _normalizePath (path) { + if (!path || typeof path !== 'string') { + return '' + } + // Remove leading and trailing slashes, then replace multiple slashes with single slash + return path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/') + } + + /** + * Build a vault path from secret name and type + * @param {string} path - Full path or secret name + * @returns {string} - Vault path + */ + buildPath (path) { + if (!path || typeof path !== 'string' || path.trim() === '') { + throw new Error(`${this.constructor.name}: buildPath() requires a non-empty path string`) + } + + const basePath = this._normalizePath(this.getBasePath()) + const normalizedPath = this._normalizePath(path) + + // If path already includes basePath, return as-is (normalized) + if (normalizedPath.startsWith(basePath + '/') || normalizedPath === basePath) { + return normalizedPath + } + + // Otherwise, prepend basePath + return `${basePath}/${normalizedPath}` + } + + /** + * Get the base path for secrets + * @returns {string} + */ + getBasePath () { + if (this.config && this.config.basePath && typeof this.config.basePath === 'string') { + return this.config.basePath + } + // Default base path (should be overridden by vault-manager, but provide fallback) + return 'controller/secrets' + } +} + +module.exports = BaseVaultProvider diff --git a/src/vault/google-secret-manager-provider.js b/src/vault/google-secret-manager-provider.js new file mode 100644 index 00000000..e26dc30b --- /dev/null +++ b/src/vault/google-secret-manager-provider.js @@ -0,0 +1,222 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseVaultProvider = require('./base-vault-provider') + +class GoogleSecretManagerProvider extends BaseVaultProvider { + constructor () { + super() + this.client = null + } + + getName () { + return 'google-secret-manager' + } + + async initialize (config) { + // Call parent initialize to set this.config + await super.initialize(config) + + if (!config.projectId) { + throw new Error('Google Cloud project ID is required') + } + + try { + const { SecretManagerServiceClient } = require('@google-cloud/secret-manager') + + // Handle credentials - can be a file path (string) or credentials object + let credentials = config.credentials + let keyFilename + + // If credentials is a string, treat it as a file path + if (typeof credentials === 'string' && credentials.trim() !== '') { + keyFilename = credentials + credentials = undefined + } + + this.client = new SecretManagerServiceClient({ + projectId: config.projectId, + keyFilename: keyFilename, + credentials: credentials + }) + + this.projectId = config.projectId + + // Test connection + await this.testConnection() + } catch (error) { + throw new Error(`Failed to initialize Google Secret Manager: ${error.message}. Make sure @google-cloud/secret-manager is installed.`) + } + } + + async store (path, data) { + const secretName = this.buildPath(path) + const secretId = this._sanitizeSecretName(secretName) + const projectPath = this.client.projectPath(this.projectId) + const secretPath = this.client.secretPath(this.projectId, secretId) + + try { + // Check if secret exists + try { + await this.client.getSecret({ name: secretPath }) + // Secret exists, create new version + const payload = Buffer.from(JSON.stringify(data)).toString('utf8') + await this.client.addSecretVersion({ + parent: secretPath, + payload: { + data: payload + } + }) + return secretId + } catch (error) { + if (error.code === 5) { // NOT_FOUND + // Create new secret + const [secret] = await this.client.createSecret({ + parent: projectPath, + secretId: secretId, + secret: { + replication: { + automatic: {} + } + } + }) + + // Add first version + const payload = Buffer.from(JSON.stringify(data)).toString('utf8') + await this.client.addSecretVersion({ + parent: secret.name, + payload: { + data: payload + } + }) + + return secretId + } + throw error + } + } catch (error) { + throw new Error(`Failed to store secret in Google Secret Manager: ${error.message}`) + } + } + + async retrieve (path) { + const secretName = this.buildPath(path) + const secretId = this._sanitizeSecretName(secretName) + const name = this.client.secretVersionPath(this.projectId, secretId, 'latest') + + try { + const [version] = await this.client.accessSecretVersion({ name }) + if (version.payload && version.payload.data) { + const data = version.payload.data.toString('utf8') + return JSON.parse(data) + } + throw new Error('Secret has no data') + } catch (error) { + if (error.code === 5) { // NOT_FOUND + throw new Error(`Secret not found: ${secretId}`) + } + throw error + } + } + + async delete (path) { + const secretName = this.buildPath(path) + const secretId = this._sanitizeSecretName(secretName) + const name = this.client.secretPath(this.projectId, secretId) + + try { + // Destroy existing versions to align with immediate delete semantics + try { + const [versions] = await this.client.listSecretVersions({ parent: name }) + for (const v of versions) { + if (v.state !== 'DESTROYED') { + try { + await this.client.destroySecretVersion({ name: v.name }) + } catch (destroyErr) { + // Ignore if already destroyed or not found + if (destroyErr.code && destroyErr.code !== 5) { + throw destroyErr + } + } + } + } + } catch (listErr) { + // If listing fails (e.g., secret absent), fall through to delete + } + + await this.client.deleteSecret({ name }) + } catch (error) { + if (error.code !== 5) { // NOT_FOUND + throw error + } + } + } + + async exists (path) { + try { + await this.retrieve(path) + return true + } catch (error) { + if (error.message.includes('not found') || error.code === 5) { + return false + } + throw error + } + } + + async list (path) { + const prefix = this._sanitizeSecretName(this.buildPath(path)) + const parent = this.client.projectPath(this.projectId) + + try { + const secrets = [] + const [secretList] = await this.client.listSecrets({ parent }) + for (const secret of secretList) { + const secretId = secret.name.split('/').pop() + if (secretId.startsWith(prefix)) { + secrets.push(secretId) + } + } + return secrets + } catch (error) { + return [] + } + } + + async testConnection () { + try { + const parent = this.client.projectPath(this.projectId) + await this.client.listSecrets({ parent }) + return true + } catch (error) { + throw new Error(`Failed to connect to Google Secret Manager: ${error.message}`) + } + } + + /** + * Sanitize secret name for Google Secret Manager + * Secret names can only contain lowercase letters, numbers, and hyphens + */ + _sanitizeSecretName (name) { + return name.replace(/[^a-z0-9-]/g, '-').toLowerCase() + } + + getBasePath () { + if (this.config && this.config.basePath && typeof this.config.basePath === 'string') { + return this.config.basePath + } + return 'pot-controller/secrets' + } +} + +module.exports = GoogleSecretManagerProvider diff --git a/src/vault/hashicorp-vault-provider.js b/src/vault/hashicorp-vault-provider.js new file mode 100644 index 00000000..1724e018 --- /dev/null +++ b/src/vault/hashicorp-vault-provider.js @@ -0,0 +1,231 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseVaultProvider = require('./base-vault-provider') +const https = require('https') +const http = require('http') +const { URL } = require('url') + +class HashiCorpVaultProvider extends BaseVaultProvider { + constructor () { + super() + this.token = null + this.client = null + this.mount = 'secret' + } + + getName () { + return 'hashicorp-vault' + } + + async initialize (config) { + // Call parent initialize to set this.config + await super.initialize(config) + + this.token = config.token + // Normalize mount (strip leading/trailing slashes) + this.mount = (config.mount || 'secret').replace(/^\/+|\/+$/g, '') + + if (!this.config.address) { + throw new Error('HashiCorp Vault address is required') + } + + if (!this.token) { + throw new Error('HashiCorp Vault token is required') + } + + // Test connection + await this.testConnection() + } + + async _makeRequest (method, path, data = null) { + const url = new URL(path, this.config.address) + const isHttps = url.protocol === 'https:' + + const options = { + method, + headers: { + 'X-Vault-Token': this.token, + 'Content-Type': 'application/json' + } + } + + if (data) { + options.headers['Content-Length'] = JSON.stringify(data).length + } + + return new Promise((resolve, reject) => { + const requestModule = isHttps ? https : http + const req = requestModule.request(url, options, (res) => { + let responseData = '' + + res.on('data', (chunk) => { + responseData += chunk + }) + + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const parsed = JSON.parse(responseData) + resolve(parsed) + } catch (error) { + resolve(responseData) + } + } else { + const error = new Error(`Vault request failed: ${res.statusCode} ${res.statusMessage}`) + error.statusCode = res.statusCode + error.response = responseData + reject(error) + } + }) + }) + + req.on('error', (error) => { + reject(error) + }) + + if (data) { + req.write(JSON.stringify(data)) + } + + req.end() + }) + } + + async store (path, data) { + const vaultPath = this.buildPath(path) + const secretPath = `v1/${this.mount}/data/${vaultPath}` + + const payload = { + data: data + } + + try { + await this._makeRequest('POST', secretPath, payload) + return vaultPath + } catch (error) { + // Try KV v1 API if v2 fails + if (error.statusCode === 404) { + const v1Path = `v1/${this.mount}/${vaultPath}` + await this._makeRequest('POST', v1Path, data) + return vaultPath + } + throw error + } + } + + async retrieve (path) { + const vaultPath = this.buildPath(path) + const secretPath = `v1/${this.mount}/data/${vaultPath}` + + try { + const response = await this._makeRequest('GET', secretPath) + // KV v2 API structure + if (response.data && response.data.data) { + return response.data.data + } + return response.data || response + } catch (error) { + // Try KV v1 API if v2 fails + if (error.statusCode === 404) { + const v1Path = `v1/${this.mount}/${vaultPath}` + const response = await this._makeRequest('GET', v1Path) + return response.data || response + } + throw error + } + } + + async delete (path) { + const vaultPath = this.buildPath(path) + const metadataPath = `v1/${this.mount}/metadata/${vaultPath}` + + try { + // KV v2 full delete (removes metadata and all versions) + await this._makeRequest('DELETE', metadataPath) + } catch (error) { + // Try KV v1 API if v2 fails / metadata not found + if (error.statusCode === 404) { + const v1Path = `v1/${this.mount}/${vaultPath}` + try { + await this._makeRequest('DELETE', v1Path) + } catch (v1Error) { + if (v1Error.statusCode !== 404) { + throw v1Error + } + // If still 404, treat as already deleted + } + } else { + throw error + } + } + } + + async exists (path) { + try { + await this.retrieve(path) + return true + } catch (error) { + if (error.statusCode === 404) { + return false + } + throw error + } + } + + async list (path) { + const vaultPath = this.buildPath(path) + const secretPath = `v1/${this.mount}/metadata/${vaultPath}` + + try { + const response = await this._makeRequest('LIST', secretPath) + if (response.data && response.data.keys) { + return response.data.keys.map(key => `${vaultPath}/${key}`) + } + return [] + } catch (error) { + // Try KV v1 API if v2 fails + if (error.statusCode === 404) { + const v1Path = `v1/${this.mount}/${vaultPath}?list=true` + try { + const response = await this._makeRequest('GET', v1Path) + if (response.data && response.data.keys) { + return response.data.keys.map(key => `${vaultPath}/${key}`) + } + } catch (listError) { + // If listing fails, return empty array + return [] + } + } + return [] + } + } + + async testConnection () { + try { + await this._makeRequest('GET', 'v1/sys/health') + return true + } catch (error) { + throw new Error(`Failed to connect to HashiCorp Vault: ${error.message}`) + } + } + + getBasePath () { + if (this.config && this.config.basePath && typeof this.config.basePath === 'string') { + return this.config.basePath + } + return 'pot-controller/secrets' + } +} + +module.exports = HashiCorpVaultProvider diff --git a/src/vault/vault-manager.js b/src/vault/vault-manager.js new file mode 100644 index 00000000..bc2c2f0f --- /dev/null +++ b/src/vault/vault-manager.js @@ -0,0 +1,305 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const HashiCorpVaultProvider = require('./hashicorp-vault-provider') +const AWSSecretsManagerProvider = require('./aws-secrets-manager-provider') +const AzureKeyVaultProvider = require('./azure-key-vault-provider') +const GoogleSecretManagerProvider = require('./google-secret-manager-provider') +const config = require('../config') +const logger = require('../logger') + +class VaultManager { + constructor () { + this.provider = null + this.initialized = false + } + + /** + * Get base path with namespace substitution and defaults + * @returns {string} + */ + _getBasePath () { + // Get basePath from env var or config, with default + const basePath = process.env.VAULT_BASE_PATH || config.get('vault.basePath', 'pot/$namespace/secrets') + const namespace = process.env.CONTROLLER_NAMESPACE || config.get('app.namespace', 'datasance') + + // Replace $namespace variable + return basePath.replace(/\$namespace/g, namespace) + } + + /** + * Validate and load HashiCorp Vault configuration + * @returns {Object} + */ + _loadHashiCorpConfig () { + const address = process.env.VAULT_HASHICORP_ADDRESS || config.get('vault.hashicorp.address') + const token = process.env.VAULT_HASHICORP_TOKEN || config.get('vault.hashicorp.token') + const mount = (process.env.VAULT_HASHICORP_MOUNT || config.get('vault.hashicorp.mount', 'secret')).replace(/^\/+|\/+$/g, '') + + if (!address) { + throw new Error('HashiCorp Vault address is required. Set VAULT_HASHICORP_ADDRESS env var or vault.hashicorp.address in config') + } + + if (!token) { + throw new Error('HashiCorp Vault token is required. Set VAULT_HASHICORP_TOKEN env var or vault.hashicorp.token in config') + } + + return { + address, + token, + mount, + basePath: this._getBasePath() + } + } + + /** + * Validate and load AWS Secrets Manager configuration + * @returns {Object} + */ + _loadAWSConfig () { + const region = process.env.VAULT_AWS_REGION || process.env.AWS_REGION || config.get('vault.aws.region', 'us-east-1') + const accessKeyId = process.env.VAULT_AWS_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID || config.get('vault.aws.accessKeyId') + const accessKey = process.env.VAULT_AWS_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY || config.get('vault.aws.accessKey') + + // Region is required + if (!region) { + throw new Error('AWS region is required. Set VAULT_AWS_REGION or AWS_REGION env var or vault.aws.region in config') + } + + // Validate AWS region format (basic validation) + // AWS regions follow pattern: us-east-1, eu-west-1, ap-southeast-1, etc. + const regionPattern = /^[a-z]{2}-[a-z]+-\d+$/ + if (!regionPattern.test(region)) { + throw new Error(`Invalid AWS region format: "${region}". AWS regions should follow the pattern: us-east-1, eu-west-1, ap-southeast-1, etc.`) + } + + // If accessKeyId is provided, accessKey must also be provided + if (accessKeyId && !accessKey) { + throw new Error('AWS secret access key is required when access key ID is provided. Set VAULT_AWS_ACCESS_KEY or AWS_SECRET_ACCESS_KEY env var or vault.aws.accessKey in config') + } + + if (accessKey && !accessKeyId) { + throw new Error('AWS access key ID is required when secret access key is provided. Set VAULT_AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_ID env var or vault.aws.accessKeyId in config') + } + + return { + region, + accessKeyId, + accessKey, + basePath: this._getBasePath() + } + } + + /** + * Validate and load Azure Key Vault configuration + * @returns {Object} + */ + _loadAzureConfig () { + const url = process.env.VAULT_AZURE_URL || config.get('vault.azure.url') + const tenantId = process.env.VAULT_AZURE_TENANT_ID || process.env.AZURE_TENANT_ID || config.get('vault.azure.tenantId') + const clientId = process.env.VAULT_AZURE_CLIENT_ID || process.env.AZURE_CLIENT_ID || config.get('vault.azure.clientId') + const clientSecret = process.env.VAULT_AZURE_CLIENT_SECRET || process.env.AZURE_CLIENT_SECRET || config.get('vault.azure.clientSecret') + + if (!url) { + throw new Error('Azure Key Vault URL is required. Set VAULT_AZURE_URL env var or vault.azure.url in config') + } + + // If any credential is provided, all must be provided + const hasCredentials = tenantId || clientId || clientSecret + if (hasCredentials) { + if (!tenantId) { + throw new Error('Azure tenant ID is required when using service principal authentication. Set VAULT_AZURE_TENANT_ID or AZURE_TENANT_ID env var or vault.azure.tenantId in config') + } + if (!clientId) { + throw new Error('Azure client ID is required when using service principal authentication. Set VAULT_AZURE_CLIENT_ID or AZURE_CLIENT_ID env var or vault.azure.clientId in config') + } + if (!clientSecret) { + throw new Error('Azure client secret is required when using service principal authentication. Set VAULT_AZURE_CLIENT_SECRET or AZURE_CLIENT_SECRET env var or vault.azure.clientSecret in config') + } + } + + return { + url, + tenantId, + clientId, + clientSecret, + basePath: this._getBasePath() + } + } + + /** + * Validate and load Google Secret Manager configuration + * @returns {Object} + */ + _loadGoogleConfig () { + const projectId = process.env.VAULT_GOOGLE_PROJECT_ID || process.env.GCP_PROJECT_ID || config.get('vault.google.projectId') + const credentials = process.env.VAULT_GOOGLE_CREDENTIALS || process.env.GOOGLE_APPLICATION_CREDENTIALS || config.get('vault.google.credentials') + + if (!projectId) { + throw new Error('Google Cloud project ID is required. Set VAULT_GOOGLE_PROJECT_ID or GCP_PROJECT_ID env var or vault.google.projectId in config') + } + + return { + projectId, + credentials, + basePath: this._getBasePath() + } + } + + /** + * Initialize the vault manager based on configuration + * @returns {Promise} + */ + async initialize () { + // Check if vault is enabled using env var or config + const vaultEnabled = process.env.VAULT_ENABLED === 'true' || config.get('vault.enabled', false) + + // If vault is not enabled, use internal encryption + if (!vaultEnabled) { + logger.info('Vault integration disabled, using internal encryption') + this.initialized = false + return + } + + // Get provider type from env var or config + const providerType = process.env.VAULT_PROVIDER || config.get('vault.provider') + + if (!providerType) { + throw new Error('Vault provider type is required when vault is enabled. Set VAULT_PROVIDER env var or vault.provider in config') + } + + let providerConfig + + // Initialize the appropriate provider and load configuration + switch (providerType.toLowerCase()) { + case 'hashicorp': + case 'vault': + case 'openbao': + this.provider = new HashiCorpVaultProvider() + providerConfig = this._loadHashiCorpConfig() + break + case 'aws-secrets-manager': + case 'aws': + this.provider = new AWSSecretsManagerProvider() + providerConfig = this._loadAWSConfig() + break + case 'azure-key-vault': + case 'azure': + this.provider = new AzureKeyVaultProvider() + providerConfig = this._loadAzureConfig() + break + case 'google-secret-manager': + case 'gcp': + case 'google': + this.provider = new GoogleSecretManagerProvider() + providerConfig = this._loadGoogleConfig() + break + default: + throw new Error(`Unsupported vault provider: ${providerType}. Supported providers: hashicorp, vault, openbao, aws, azure, google`) + } + + // Initialize provider with validated configuration + try { + await this.provider.initialize(providerConfig) + this.initialized = true + logger.info(`Vault integration enabled with provider: ${providerType}`) + } catch (error) { + // Re-throw with context + throw new Error(`Failed to initialize vault provider '${providerType}': ${error.message}`) + } + } + + /** + * Check if vault is enabled and initialized + * @returns {boolean} + */ + isEnabled () { + return this.initialized && this.provider !== null + } + + /** + * Store a secret in the vault + * @param {string} secretName - Secret name + * @param {string} secretType - Secret type (optional) + * @param {Object} data - Secret data + * @returns {Promise} - Returns vault reference (full path) + */ + async store (secretName, secretType, data) { + if (!this.isEnabled()) { + throw new Error('Vault is not enabled') + } + + // Build path: secretType/secretName or just secretName + const path = secretType ? `${secretType}/${secretName}` : secretName + // Provider's buildPath will prepend basePath + return this.provider.store(path, data) + } + + /** + * Retrieve a secret from the vault + * @param {string} secretName - Secret name or full vault path + * @param {string} secretType - Secret type (optional) + * @returns {Promise} - Returns secret data + */ + async retrieve (secretName, secretType) { + if (!this.isEnabled()) { + throw new Error('Vault is not enabled') + } + + // If secretName looks like a full path (contains /), use it directly + // Otherwise build path from secretType and secretName + const path = secretName.includes('/') ? secretName : (secretType ? `${secretType}/${secretName}` : secretName) + return this.provider.retrieve(path) + } + + /** + * Delete a secret from the vault + * @param {string} secretName - Secret name + * @param {string} secretType - Secret type (optional) + * @returns {Promise} + */ + async delete (secretName, secretType) { + if (!this.isEnabled()) { + throw new Error('Vault is not enabled') + } + + const path = secretType ? `${secretType}/${secretName}` : secretName + return this.provider.delete(path) + } + + /** + * Check if a secret exists in the vault + * @param {string} secretName - Secret name + * @param {string} secretType - Secret type (optional) + * @returns {Promise} + */ + async exists (secretName, secretType) { + if (!this.isEnabled()) { + return false + } + + const path = secretType ? `${secretType}/${secretName}` : secretName + return this.provider.exists(path) + } + + /** + * Get the provider instance + * @returns {BaseVaultProvider|null} + */ + getProvider () { + return this.provider + } +} + +// Export singleton instance +module.exports = new VaultManager() From 4e192e153f70e5850cabd5bd642ba5d8cdb48b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Sat, 3 Jan 2026 00:29:09 +0300 Subject: [PATCH 2/4] new ws endpoints for agent and microservice logs --- package-lock.json | 134 +-- package.json | 6 +- src/controllers/agent-controller.js | 7 +- src/data/managers/fog-log-status-manager.js | 37 + .../microservice-log-status-manager.js | 37 + .../mysql/db_migration_mysql_v1.0.7.sql | 38 + .../postgres/db_migration_pg_v1.0.7.sql | 38 + .../sqlite/db_migration_sqlite_v1.0.7.sql | 39 +- src/data/models/changetracking.js | 10 + src/data/models/fogLogStatus.js | 63 ++ src/data/models/microserviceLogStatus.js | 63 ++ src/routes/agent.js | 112 +++ src/routes/iofog.js | 44 + src/routes/microservices.js | 43 + src/services/agent-service.js | 62 +- src/services/change-tracking-service.js | 6 + src/services/websocket-queue-service.js | 240 +++++ src/websocket/log-session-manager.js | 203 ++++ src/websocket/server.js | 903 +++++++++++++++++- 19 files changed, 1955 insertions(+), 130 deletions(-) create mode 100644 src/data/managers/fog-log-status-manager.js create mode 100644 src/data/managers/microservice-log-status-manager.js create mode 100644 src/data/models/fogLogStatus.js create mode 100644 src/data/models/microserviceLogStatus.js create mode 100644 src/websocket/log-session-manager.js diff --git a/package-lock.json b/package-lock.json index a76d1f6b..6a46bcae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@opentelemetry/sdk-node": "^0.200.0", "axios": "1.12.2", "bignumber.js": "^9.3.0", - "body-parser": "^1.20.3", + "body-parser": "^1.20.4", "command-line-args": "5.2.1", "command-line-usage": "7.0.3", "concurrent-queue": "7.0.2", @@ -34,7 +34,7 @@ "daemonize2": "0.4.2", "dotenv": "^16.5.0", "ejs": "3.1.10", - "express": "4.21.2", + "express": "4.22.1", "express-session": "1.18.2", "formidable": "3.5.4", "ftp": "0.3.10", @@ -54,7 +54,7 @@ "pino": "9.13.1", "pino-std-serializers": "7.0.0", "portscanner": "2.2.0", - "qs": "6.12.1", + "qs": "6.14.1", "rhea": "^3.0.4", "sequelize": "6.37.7", "sqlite3": "^5.1.7", @@ -4254,20 +4254,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -6672,38 +6658,38 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -6739,80 +6725,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -12873,11 +12785,11 @@ } }, "node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" diff --git a/package.json b/package.json index 7c80da15..5abc4eee 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@opentelemetry/sdk-node": "^0.200.0", "axios": "1.12.2", "bignumber.js": "^9.3.0", - "body-parser": "^1.20.3", + "body-parser": "^1.20.4", "command-line-args": "5.2.1", "command-line-usage": "7.0.3", "concurrent-queue": "7.0.2", @@ -79,7 +79,7 @@ "daemonize2": "0.4.2", "dotenv": "^16.5.0", "ejs": "3.1.10", - "express": "4.21.2", + "express": "4.22.1", "express-session": "1.18.2", "formidable": "3.5.4", "ftp": "0.3.10", @@ -99,7 +99,7 @@ "pino": "9.13.1", "pino-std-serializers": "7.0.0", "portscanner": "2.2.0", - "qs": "6.12.1", + "qs": "6.14.1", "rhea": "^3.0.4", "sequelize": "6.37.7", "sqlite3": "^5.1.7", diff --git a/src/controllers/agent-controller.js b/src/controllers/agent-controller.js index 404be92c..e1aff0c2 100644 --- a/src/controllers/agent-controller.js +++ b/src/controllers/agent-controller.js @@ -68,6 +68,10 @@ const getAgentLinkedVolumeMountsEndpoint = async function (req, fog) { return { volumeMounts: await AgentService.getAgentLinkedVolumeMounts(fog) } } +const getAgentLogSessionsEndPoint = async function (req, fog) { + return AgentService.getAgentLogSessions(fog) +} + const getAgentMicroserviceEndPoint = async function (req, fog) { const microserviceUuid = req.params.microserviceUuid @@ -147,5 +151,6 @@ module.exports = { resetAgentConfigChangesEndPoint: AuthDecorator.checkFogToken(resetAgentConfigChangesEndPoint), getAgentLinkedEdgeResourcesEndpoint: AuthDecorator.checkFogToken(getAgentLinkedEdgeResourcesEndpoint), getAgentLinkedVolumeMountsEndpoint: AuthDecorator.checkFogToken(getAgentLinkedVolumeMountsEndpoint), - getControllerCAEndPoint: AuthDecorator.checkFogToken(getControllerCAEndPoint) + getControllerCAEndPoint: AuthDecorator.checkFogToken(getControllerCAEndPoint), + getAgentLogSessionsEndPoint: AuthDecorator.checkFogToken(getAgentLogSessionsEndPoint) } diff --git a/src/data/managers/fog-log-status-manager.js b/src/data/managers/fog-log-status-manager.js new file mode 100644 index 00000000..34f3c28f --- /dev/null +++ b/src/data/managers/fog-log-status-manager.js @@ -0,0 +1,37 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const models = require('../models') +const FogLogStatus = models.FogLogStatus + +const fogLogStatusExcludedFields = [ + 'id', + 'iofog_uuid', + 'iofogUuid', + 'created_at', + 'updated_at' +] + +class FogLogStatusManager extends BaseManager { + getEntity () { + return FogLogStatus + } + + findAllExcludeFields (where, transaction) { + return this.findAllWithAttributes(where, { exclude: fogLogStatusExcludedFields }, transaction) + } +} + +const instance = new FogLogStatusManager() +module.exports = instance diff --git a/src/data/managers/microservice-log-status-manager.js b/src/data/managers/microservice-log-status-manager.js new file mode 100644 index 00000000..deaa3ff6 --- /dev/null +++ b/src/data/managers/microservice-log-status-manager.js @@ -0,0 +1,37 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const models = require('../models') +const MicroserviceLogStatus = models.MicroserviceLogStatus + +const microserviceLogStatusExcludedFields = [ + 'id', + 'microservice_uuid', + 'microserviceUuid', + 'created_at', + 'updated_at' +] + +class MicroserviceLogStatusManager extends BaseManager { + getEntity () { + return MicroserviceLogStatus + } + + findAllExcludeFields (where, transaction) { + return this.findAllWithAttributes(where, { exclude: microserviceLogStatusExcludedFields }, transaction) + } +} + +const instance = new MicroserviceLogStatusManager() +module.exports = instance diff --git a/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql b/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql index 63c26c32..4fe961fb 100644 --- a/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql +++ b/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql @@ -843,4 +843,42 @@ CREATE INDEX idx_events_created_at ON Events (created_at); ALTER TABLE ConfigMaps ADD COLUMN use_vault BOOLEAN DEFAULT true; +CREATE TABLE IF NOT EXISTS MicroserviceLogStatuses ( + id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + microservice_uuid VARCHAR(36), + log_session_id TEXT, + session_id VARCHAR(255) UNIQUE NOT NULL, + status TEXT, + tail_config TEXT, + agent_connected BOOLEAN DEFAULT false, + user_connected BOOLEAN DEFAULT false, + created_at DATETIME, + updated_at DATETIME, + FOREIGN KEY (microservice_uuid) REFERENCES Microservices (uuid) ON DELETE CASCADE +); + +CREATE INDEX idx_microservice_log_status_microservice_uuid ON MicroserviceLogStatuses (microservice_uuid); +CREATE INDEX idx_microservice_log_status_session_id ON MicroserviceLogStatuses (session_id); + +CREATE TABLE IF NOT EXISTS FogLogStatuses ( + id INT AUTO_INCREMENT PRIMARY KEY NOT NULL, + iofog_uuid VARCHAR(36), + log_session_id TEXT, + session_id VARCHAR(255) UNIQUE NOT NULL, + status TEXT, + tail_config TEXT, + agent_connected BOOLEAN DEFAULT false, + user_connected BOOLEAN DEFAULT false, + created_at DATETIME, + updated_at DATETIME, + FOREIGN KEY (iofog_uuid) REFERENCES Fogs (uuid) ON DELETE CASCADE +); + +CREATE INDEX idx_fog_log_status_iofog_uuid ON FogLogStatuses (iofog_uuid); +CREATE INDEX idx_fog_log_status_session_id ON FogLogStatuses (session_id); + +ALTER TABLE ChangeTrackings ADD COLUMN microservice_logs BOOLEAN DEFAULT false; +ALTER TABLE ChangeTrackings ADD COLUMN fog_logs BOOLEAN DEFAULT false; + + COMMIT; \ No newline at end of file diff --git a/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql b/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql index 774c714f..b53ffc86 100644 --- a/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql +++ b/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql @@ -843,3 +843,41 @@ CREATE INDEX idx_events_event_type ON "Events" (event_type); CREATE INDEX idx_events_created_at ON "Events" (created_at); ALTER TABLE "ConfigMaps" ADD COLUMN use_vault BOOLEAN DEFAULT true; + +CREATE TABLE IF NOT EXISTS "MicroserviceLogStatuses" ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL, + microservice_uuid VARCHAR(36), + log_session_id TEXT, + session_id VARCHAR(255) UNIQUE NOT NULL, + status TEXT, + tail_config TEXT, + agent_connected BOOLEAN DEFAULT false, + user_connected BOOLEAN DEFAULT false, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0), + FOREIGN KEY (microservice_uuid) REFERENCES "Microservices" (uuid) ON DELETE CASCADE +); + +CREATE INDEX idx_microservice_log_status_microservice_uuid ON "MicroserviceLogStatuses" (microservice_uuid); +CREATE INDEX idx_microservice_log_status_session_id ON "MicroserviceLogStatuses" (session_id); + +CREATE TABLE IF NOT EXISTS "FogLogStatuses" ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL, + iofog_uuid VARCHAR(36), + log_session_id TEXT, + session_id VARCHAR(255) UNIQUE NOT NULL, + status TEXT, + tail_config TEXT, + agent_connected BOOLEAN DEFAULT false, + user_connected BOOLEAN DEFAULT false, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0), + FOREIGN KEY (iofog_uuid) REFERENCES "Fogs" (uuid) ON DELETE CASCADE +); + +CREATE INDEX idx_fog_log_status_iofog_uuid ON "FogLogStatuses" (iofog_uuid); +CREATE INDEX idx_fog_log_status_session_id ON "FogLogStatuses" (session_id); + +ALTER TABLE "ChangeTrackings" ADD COLUMN microservice_logs BOOLEAN DEFAULT false; +ALTER TABLE "ChangeTrackings" ADD COLUMN fog_logs BOOLEAN DEFAULT false; + diff --git a/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql b/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql index ba4d8171..b69097cd 100644 --- a/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql +++ b/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql @@ -828,4 +828,41 @@ CREATE INDEX IF NOT EXISTS idx_events_method ON Events (method); CREATE INDEX IF NOT EXISTS idx_events_event_type ON Events (event_type); CREATE INDEX IF NOT EXISTS idx_events_created_at ON Events (created_at); -ALTER TABLE ConfigMaps ADD COLUMN use_vault BOOLEAN DEFAULT true; \ No newline at end of file +ALTER TABLE ConfigMaps ADD COLUMN use_vault BOOLEAN DEFAULT true; + +CREATE TABLE IF NOT EXISTS MicroserviceLogStatuses ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + microservice_uuid VARCHAR(36), + log_session_id TEXT, + session_id TEXT UNIQUE NOT NULL, + status TEXT, + tail_config TEXT, + agent_connected BOOLEAN DEFAULT false, + user_connected BOOLEAN DEFAULT false, + created_at DATETIME, + updated_at DATETIME, + FOREIGN KEY (microservice_uuid) REFERENCES Microservices (uuid) ON DELETE CASCADE +); + +CREATE INDEX idx_microservice_log_status_microservice_uuid ON MicroserviceLogStatuses (microservice_uuid); +CREATE INDEX idx_microservice_log_status_session_id ON MicroserviceLogStatuses (session_id); + +CREATE TABLE IF NOT EXISTS FogLogStatuses ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + iofog_uuid VARCHAR(36), + log_session_id TEXT, + session_id TEXT UNIQUE NOT NULL, + status TEXT, + tail_config TEXT, + agent_connected BOOLEAN DEFAULT false, + user_connected BOOLEAN DEFAULT false, + created_at DATETIME, + updated_at DATETIME, + FOREIGN KEY (iofog_uuid) REFERENCES Fogs (uuid) ON DELETE CASCADE +); + +CREATE INDEX idx_fog_log_status_iofog_uuid ON FogLogStatuses (iofog_uuid); +CREATE INDEX idx_fog_log_status_session_id ON FogLogStatuses (session_id); + +ALTER TABLE ChangeTrackings ADD COLUMN microservice_logs BOOLEAN DEFAULT false; +ALTER TABLE ChangeTrackings ADD COLUMN fog_logs BOOLEAN DEFAULT false; diff --git a/src/data/models/changetracking.js b/src/data/models/changetracking.js index a3932eb9..4b303c01 100644 --- a/src/data/models/changetracking.js +++ b/src/data/models/changetracking.js @@ -101,6 +101,16 @@ module.exports = (sequelize, DataTypes) => { field: 'exec_sessions', defaultValue: false }, + microserviceLogs: { + type: DataTypes.BOOLEAN, + field: 'microservice_logs', + defaultValue: false + }, + fogLogs: { + type: DataTypes.BOOLEAN, + field: 'fog_logs', + defaultValue: false + }, lastUpdated: { type: DataTypes.STRING, field: 'last_updated', diff --git a/src/data/models/fogLogStatus.js b/src/data/models/fogLogStatus.js new file mode 100644 index 00000000..1c9c20e1 --- /dev/null +++ b/src/data/models/fogLogStatus.js @@ -0,0 +1,63 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const FogLogStatus = sequelize.define('FogLogStatus', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'id' + }, + iofogUuid: { + type: DataTypes.STRING(36), + field: 'iofog_uuid', + allowNull: true + }, + logSessionId: { + type: DataTypes.TEXT, + field: 'log_session_id', + allowNull: true + }, + sessionId: { + type: DataTypes.TEXT, + field: 'session_id', + allowNull: false, + unique: true + }, + status: { + type: DataTypes.TEXT, + field: 'status', + allowNull: true + }, + tailConfig: { + type: DataTypes.TEXT, + field: 'tail_config', + allowNull: true + }, + agentConnected: { + type: DataTypes.BOOLEAN, + field: 'agent_connected', + defaultValue: false + }, + userConnected: { + type: DataTypes.BOOLEAN, + field: 'user_connected', + defaultValue: false + } + }, { + tableName: 'FogLogStatuses', + timestamps: true, + underscored: true + }) + FogLogStatus.associate = function (models) { + FogLogStatus.belongsTo(models.Fog, { + foreignKey: { + name: 'iofogUuid', + field: 'iofog_uuid' + }, + as: 'fog', + onDelete: 'cascade' + }) + } + return FogLogStatus +} diff --git a/src/data/models/microserviceLogStatus.js b/src/data/models/microserviceLogStatus.js new file mode 100644 index 00000000..a6e41aa0 --- /dev/null +++ b/src/data/models/microserviceLogStatus.js @@ -0,0 +1,63 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const MicroserviceLogStatus = sequelize.define('MicroserviceLogStatus', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'id' + }, + microserviceUuid: { + type: DataTypes.STRING(36), + field: 'microservice_uuid', + allowNull: true + }, + logSessionId: { + type: DataTypes.TEXT, + field: 'log_session_id', + allowNull: true + }, + sessionId: { + type: DataTypes.TEXT, + field: 'session_id', + allowNull: false, + unique: true + }, + status: { + type: DataTypes.TEXT, + field: 'status', + allowNull: true + }, + tailConfig: { + type: DataTypes.TEXT, + field: 'tail_config', + allowNull: true + }, + agentConnected: { + type: DataTypes.BOOLEAN, + field: 'agent_connected', + defaultValue: false + }, + userConnected: { + type: DataTypes.BOOLEAN, + field: 'user_connected', + defaultValue: false + } + }, { + tableName: 'MicroserviceLogStatuses', + timestamps: true, + underscored: true + }) + MicroserviceLogStatus.associate = function (models) { + MicroserviceLogStatus.belongsTo(models.Microservice, { + foreignKey: { + name: 'microserviceUuid', + field: 'microservice_uuid' + }, + as: 'microservice', + onDelete: 'cascade' + }) + } + return MicroserviceLogStatus +} diff --git a/src/routes/agent.js b/src/routes/agent.js index 8aca966f..980098d4 100644 --- a/src/routes/agent.js +++ b/src/routes/agent.js @@ -668,6 +668,30 @@ module.exports = [ logger.apiRes({ req: req, res: res, responseObject: responseObject }) } }, + { + method: 'get', + path: '/api/v3/agent/logs/sessions', + middleware: async (req, res) => { + logger.apiReq(req) + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + const getAgentLogSessionsEndPoint = ResponseDecorator.handleErrors(AgentController.getAgentLogSessionsEndPoint, + successCode, errorCodes) + const responseObject = await getAgentLogSessionsEndPoint(req) + + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, res: res, responseObject: responseObject }) + } + }, { method: 'ws', path: '/api/v3/agent/exec/:microserviceUuid', @@ -710,5 +734,93 @@ module.exports = [ } } } + }, + { + method: 'ws', + path: '/api/v3/agent/logs/microservice/:microserviceUuid/:sessionId', + middleware: async (ws, req) => { + logger.apiReq(req) + try { + const token = req.headers.authorization + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:' + JSON.stringify({ + error: error.message, + originalError: 'Missing authentication token' + })) + } + return + } + + // Initialize WebSocket connection for agent microservice logs + const wsServer = WebSocketServer.getInstance() + await wsServer.handleConnection(ws, req) + } catch (error) { + logger.error('Error in agent microservice logs WebSocket connection:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + microserviceUuid: req.params.microserviceUuid, + sessionId: req.params.sessionId + })) + try { + if (ws.readyState === ws.OPEN) { + ws.close(1008, error.message || 'Authentication failed') + } + } catch (closeError) { + logger.error('Error closing agent microservice logs WebSocket:' + JSON.stringify({ + error: closeError.message, + originalError: error.message + })) + } + } + } + }, + { + method: 'ws', + path: '/api/v3/agent/logs/iofog/:iofogUuid/:sessionId', + middleware: async (ws, req) => { + logger.apiReq(req) + try { + const token = req.headers.authorization + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:' + JSON.stringify({ + error: error.message, + originalError: 'Missing authentication token' + })) + } + return + } + + // Initialize WebSocket connection for agent fog logs + const wsServer = WebSocketServer.getInstance() + await wsServer.handleConnection(ws, req) + } catch (error) { + logger.error('Error in agent fog logs WebSocket connection:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + iofogUuid: req.params.iofogUuid, + sessionId: req.params.sessionId + })) + try { + if (ws.readyState === ws.OPEN) { + ws.close(1008, error.message || 'Authentication failed') + } + } catch (closeError) { + logger.error('Error closing agent fog logs WebSocket:' + JSON.stringify({ + error: closeError.message, + originalError: error.message + })) + } + } + } } ] diff --git a/src/routes/iofog.js b/src/routes/iofog.js index aae10cab..fe46d1d0 100644 --- a/src/routes/iofog.js +++ b/src/routes/iofog.js @@ -445,5 +445,49 @@ module.exports = [ logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) }) } + }, + { + method: 'ws', + path: '/api/v3/iofog/:uuid/logs', + middleware: async (ws, req) => { + const WebSocketServer = require('../websocket/server') + logger.apiReq(req) + try { + const token = req.headers.authorization + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:' + JSON.stringify({ + error: error.message, + originalError: 'Missing authentication token' + })) + } + return + } + + // Initialize WebSocket connection for fog logs + const wsServer = WebSocketServer.getInstance() + await wsServer.handleConnection(ws, req) + } catch (error) { + logger.error('Error in fog logs WebSocket connection:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + iofogUuid: req.params.uuid + })) + try { + if (ws.readyState === ws.OPEN) { + ws.close(1008, error.message || 'Authentication failed') + } + } catch (closeError) { + logger.error('Error closing fog logs WebSocket:' + JSON.stringify({ + error: closeError.message, + originalError: error.message + })) + } + } + } } ] diff --git a/src/routes/microservices.js b/src/routes/microservices.js index 8927db9b..6ecad2d2 100644 --- a/src/routes/microservices.js +++ b/src/routes/microservices.js @@ -1402,5 +1402,48 @@ module.exports = [ } } } + }, + { + method: 'ws', + path: '/api/v3/microservices/:uuid/logs', + middleware: async (ws, req) => { + logger.apiReq(req) + try { + const token = req.headers.authorization + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:' + JSON.stringify({ + error: error.message, + originalError: 'Missing authentication token' + })) + } + return + } + + // Initialize WebSocket connection for microservice logs + const wsServer = WebSocketServer.getInstance() + await wsServer.handleConnection(ws, req) + } catch (error) { + logger.error('Error in microservice logs WebSocket connection:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + microserviceUuid: req.params.uuid + })) + try { + if (ws.readyState === ws.OPEN) { + ws.close(1008, error.message || 'Authentication failed') + } + } catch (closeError) { + logger.error('Error closing microservice logs WebSocket:' + JSON.stringify({ + error: closeError.message, + originalError: error.message + })) + } + } + } } ] diff --git a/src/services/agent-service.js b/src/services/agent-service.js index 3bb1f822..fac9bee5 100644 --- a/src/services/agent-service.js +++ b/src/services/agent-service.js @@ -46,10 +46,12 @@ const EdgeResourceService = require('./edge-resource-service') const constants = require('../helpers/constants') const SecretManager = require('../data/managers/secret-manager') const ConfigMapManager = require('../data/managers/config-map-manager') +const MicroserviceLogStatusManager = require('../data/managers/microservice-log-status-manager') +const FogLogStatusManager = require('../data/managers/fog-log-status-manager') const IncomingForm = formidable.IncomingForm const CHANGE_TRACKING_DEFAULT = {} -const CHANGE_TRACKING_KEYS = ['config', 'version', 'reboot', 'deleteNode', 'microserviceList', 'microserviceConfig', 'routing', 'registries', 'tunnel', 'diagnostics', 'isImageSnapshot', 'prune', 'routerChanged', 'linkedEdgeResources', 'volumeMounts', 'execSessions'] +const CHANGE_TRACKING_KEYS = ['config', 'version', 'reboot', 'deleteNode', 'microserviceList', 'microserviceConfig', 'routing', 'registries', 'tunnel', 'diagnostics', 'isImageSnapshot', 'prune', 'routerChanged', 'linkedEdgeResources', 'volumeMounts', 'execSessions', 'microserviceLogs', 'fogLogs'] for (const key of CHANGE_TRACKING_KEYS) { CHANGE_TRACKING_DEFAULT[key] = false } @@ -739,6 +741,61 @@ const getControllerCA = async function (fog, transaction) { throw new Errors.ValidationError('No valid SSL certificate configuration found') } +// New endpoint: Get active log sessions for agent +const getAgentLogSessions = async function (fog, transaction) { + const Op = require('sequelize').Op + + // Get all microservices for this fog + const microservices = await MicroserviceManager.findAll( + { iofogUuid: fog.uuid }, + transaction + ) + + const allSessions = [] + + // Get microservice log sessions + for (const ms of microservices) { + const sessions = await MicroserviceLogStatusManager.findAll( + { + microserviceUuid: ms.uuid, + status: { [Op.in]: ['PENDING', 'ACTIVE'] } + }, + transaction + ) + + for (const session of sessions) { + allSessions.push({ + microserviceUuid: ms.uuid, + sessionId: session.sessionId, + tailConfig: JSON.parse(session.tailConfig), + status: session.status, + agentConnected: session.agentConnected + }) + } + } + + // Get fog node log sessions + const fogSessions = await FogLogStatusManager.findAll( + { + iofogUuid: fog.uuid, + status: { [Op.in]: ['PENDING', 'ACTIVE'] } + }, + transaction + ) + + for (const session of fogSessions) { + allSessions.push({ + iofogUuid: fog.uuid, + sessionId: session.sessionId, + tailConfig: JSON.parse(session.tailConfig), + status: session.status, + agentConnected: session.agentConnected + }) + } + + return { logSessions: allSessions } +} + const getAgentLinkedVolumeMounts = async function (fog, transaction) { const volumeMounts = [] const resourceAttributes = [ @@ -819,5 +876,6 @@ module.exports = { putImageSnapshot: TransactionDecorator.generateTransaction(putImageSnapshot), getAgentLinkedEdgeResources: TransactionDecorator.generateTransaction(getAgentLinkedEdgeResources), getAgentLinkedVolumeMounts: TransactionDecorator.generateTransaction(getAgentLinkedVolumeMounts), - getControllerCA: TransactionDecorator.generateTransaction(getControllerCA) + getControllerCA: TransactionDecorator.generateTransaction(getControllerCA), + getAgentLogSessions: TransactionDecorator.generateTransaction(getAgentLogSessions) } diff --git a/src/services/change-tracking-service.js b/src/services/change-tracking-service.js index d7e64aeb..c8b6b890 100644 --- a/src/services/change-tracking-service.js +++ b/src/services/change-tracking-service.js @@ -89,6 +89,12 @@ const events = Object.freeze({ }, microserviceExecSessions: { execSessions: true + }, + microserviceLogs: { + microserviceLogs: true + }, + fogLogs: { + fogLogs: true } }) diff --git a/src/services/websocket-queue-service.js b/src/services/websocket-queue-service.js index 8c962a14..3f7e436c 100644 --- a/src/services/websocket-queue-service.js +++ b/src/services/websocket-queue-service.js @@ -35,6 +35,7 @@ function getBufferFromBody (body) { class WebSocketQueueService { constructor () { this.execBridges = new Map() + this.logBridges = new Map() // New: for log sessions } async enableForSession (session, cleanupCallback) { @@ -386,6 +387,245 @@ class WebSocketQueueService { } } } + + // ========== Log Session Queue Methods ========== + + // Enable queue bridge for log session (unidirectional: agent → user, one-to-one) + // Following exec session pattern: Use queues for ALL scenarios + // Each sessionId has its own queues (one-to-one, like exec sessions) + async enableForLogSession (session, cleanupCallback) { + const sessionId = session.sessionId + if (!sessionId) { + logger.warn('[AMQP][QUEUE] Missing sessionId for log session, skipping queue bridge enablement') + return false + } + + const bridge = this.logBridges.get(sessionId) || { + sessionId, + agentSender: null, + agentReceiver: null, + userReceiver: null, // Single user receiver (one-to-one) + userSender: null, // User queue sender + cleanupCallback: null + } + + if (cleanupCallback) { + bridge.cleanupCallback = cleanupCallback + } + + // Agent side: single receiver (agent receives from queue) + if (session.agent) { + await this._ensureLogAgentReceiver(bridge, session.agent, session) + } + + // User side: single receiver (one-to-one, like exec sessions) + if (session.user) { + await this._ensureLogUserReceiver(bridge, session.user, session) + } + + this.logBridges.set(sessionId, bridge) + return true + } + + shouldUseQueueForLogs (sessionId) { + return this.logBridges.has(sessionId) + } + + // Forward to user via queue (one-to-one, like exec sessions) + // Note: Exec sessions use queues for BOTH single and multi-replica + // We follow the same pattern for consistency + // Buffer is already MessagePack encoded from agent + async publishLogToUser (sessionId, buffer, options = {}) { + const bridge = this.logBridges.get(sessionId) + if (!bridge) { + throw new Error(`Log bridge missing for sessionId=${sessionId}`) + } + + // Ensure user queue sender exists + if (!bridge.userSender) { + const userQueueName = `logs-user-${sessionId}` + const connection = await RouterConnectionService.getConnection() + const sender = await new Promise((resolve, reject) => { + const link = connection.open_sender({ + target: { + address: userQueueName, + durable: 0, + expiry_policy: 'link-detach' + }, + autosettle: true + }) + + link.once('sender_open', () => resolve(link)) + link.once('sender_close', reject) + link.once('error', reject) + }) + bridge.userSender = { sender } + } + + // Buffer is already MessagePack encoded, send as binary + const message = { + body: buffer, // MessagePack encoded buffer + content_type: 'application/octet-stream', + application_properties: options.applicationProperties || {} + } + + try { + bridge.userSender.sender.send(message) + logger.debug('[AMQP][QUEUE] Published log message to user queue', { + sessionId, + messageSize: buffer.length + }) + } catch (error) { + logger.error('[AMQP][QUEUE] Failed to publish log message to user queue', { + sessionId, + error: error.message + }) + throw error + } + } + + // Setup receiver for agent queue (one-to-one per sessionId) + async _ensureLogAgentReceiver (bridge, agentWs, session) { + if (bridge.agentReceiver) { + bridge.agentReceiver.socket = agentWs + return + } + + // Queue name per sessionId (one-to-one) + const queueName = `logs-agent-${session.sessionId}` + const connection = await RouterConnectionService.getConnection() + + const receiver = await new Promise((resolve, reject) => { + const link = connection.open_receiver({ + source: { + address: queueName, + durable: 0, + expiry_policy: 'link-detach' + }, + credit_window: 50 + }) + + link.once('receiver_open', () => resolve(link)) + link.once('receiver_close', reject) + link.once('error', reject) + }) + + receiver.on('message', async (context) => { + const currentBridge = this.logBridges.get(session.sessionId) + const ws = currentBridge && currentBridge.agentReceiver ? currentBridge.agentReceiver.socket : null + const body = getBufferFromBody(context.message.body) + + // Body is already MessagePack encoded from agent + // Forward directly to agent WebSocket (binary) + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(body, { binary: true }) + context.delivery.accept() + } else { + context.delivery.release() + } + }) + + bridge.agentReceiver = { receiver, socket: agentWs } + } + + // Setup sender for agent queue (one-to-one per sessionId) + async _ensureLogAgentSender (sessionId) { + const bridge = this.logBridges.get(sessionId) + if (!bridge) return null + if (bridge.agentSender) return bridge.agentSender + + // Queue name per sessionId (one-to-one) + const queueName = `logs-agent-${sessionId}` + const connection = await RouterConnectionService.getConnection() + + const sender = await new Promise((resolve, reject) => { + const link = connection.open_sender({ + target: { + address: queueName, + durable: 0, + expiry_policy: 'link-detach' + }, + autosettle: true + }) + + link.once('sender_open', () => resolve(link)) + link.once('sender_close', reject) + link.once('error', reject) + }) + + bridge.agentSender = { sender } + return bridge.agentSender + } + + // Setup receiver for user queue (one-to-one, like exec session pattern) + async _ensureLogUserReceiver (bridge, userWs, session) { + if (bridge.userReceiver) { + bridge.userReceiver.socket = userWs + return + } + + // Queue name per sessionId (one-to-one) + const queueName = `logs-user-${session.sessionId}` + const connection = await RouterConnectionService.getConnection() + + const receiver = await new Promise((resolve, reject) => { + const link = connection.open_receiver({ + source: { + address: queueName, + durable: 0, + expiry_policy: 'link-detach' + }, + credit_window: 50 + }) + + link.once('receiver_open', () => resolve(link)) + link.once('receiver_close', reject) + link.once('error', reject) + }) + + receiver.on('message', async (context) => { + const currentBridge = this.logBridges.get(session.sessionId) + const ws = currentBridge && currentBridge.userReceiver ? currentBridge.userReceiver.socket : null + const body = getBufferFromBody(context.message.body) + + // Body is MessagePack encoded (from agent via controller) + // Forward directly to user WebSocket (binary) + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(body, { binary: true }) + context.delivery.accept() + } else { + context.delivery.release() + } + }) + + bridge.userReceiver = { receiver, socket: userWs } + } + + // Cleanup log session (one-to-one) + async cleanupLogSession (sessionId) { + const bridge = this.logBridges.get(sessionId) + if (!bridge) return + + const closeLink = (link) => { + if (!link) return + try { + if (link.receiver) { + link.receiver.close() + } else if (link.sender) { + link.sender.close() + } + } catch (error) { + logger.debug('[AMQP][QUEUE] Failed to close log link during cleanup', { sessionId, error: error.message }) + } + } + + closeLink(bridge.agentReceiver) + closeLink(bridge.agentSender) + closeLink(bridge.userReceiver) + closeLink(bridge.userSender) + + this.logBridges.delete(sessionId) + } } module.exports = new WebSocketQueueService() diff --git a/src/websocket/log-session-manager.js b/src/websocket/log-session-manager.js new file mode 100644 index 00000000..9998b772 --- /dev/null +++ b/src/websocket/log-session-manager.js @@ -0,0 +1,203 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const WebSocket = require('ws') +const logger = require('../logger') +const MicroserviceLogStatusManager = require('../data/managers/microservice-log-status-manager') +const FogLogStatusManager = require('../data/managers/fog-log-status-manager') +const ChangeTrackingService = require('../services/change-tracking-service') +const FogManager = require('../data/managers/iofog-manager') +const MicroserviceManager = require('../data/managers/microservice-manager') + +class LogSessionManager { + constructor (config) { + if (!config || !config.session) { + const error = new Error('Invalid session manager configuration') + logger.error('Failed to initialize LogSessionManager:' + error) + throw error + } + this.logSessions = new Map() // Map + this.config = config + this.cleanupInterval = null + this.startCleanupInterval() + logger.info('LogSessionManager initialized with config:' + JSON.stringify({ + sessionTimeout: config.session.timeout, + cleanupInterval: config.session.cleanupInterval + })) + } + + createLogSession (sessionId, microserviceUuid, fogUuid, agentWs, userWs, tailConfig, transaction) { + const session = { + sessionId, // Unique per user session + microserviceUuid, + fogUuid, + agent: agentWs, + user: userWs, // Single user per session (one-to-one) + tailConfig: tailConfig, // Per-session tail configuration + lastActivity: Date.now(), + createdAt: Date.now(), + transaction + } + this.logSessions.set(sessionId, session) + return session + } + + getLogSession (sessionId) { + return this.logSessions.get(sessionId) || null + } + + getAllSessionsForLogSource (microserviceUuid, fogUuid) { + const sessions = [] + for (const [, session] of this.logSessions) { + if ((fogUuid && session.fogUuid === fogUuid) || + (microserviceUuid && session.microserviceUuid === microserviceUuid)) { + sessions.push(session) + } + } + return sessions + } + + updateLastActivity (sessionId) { + const session = this.logSessions.get(sessionId) + if (session) { + session.lastActivity = Date.now() + } + } + + async removeLogSession (sessionId, transaction) { + const session = this.logSessions.get(sessionId) + if (!session) return + + // Close connections + if (session.agent && session.agent.readyState === WebSocket.OPEN) { + session.agent.close() + } + if (session.user && session.user.readyState === WebSocket.OPEN) { + session.user.close() + } + this.logSessions.delete(sessionId) + + // Remove from database + try { + if (session.microserviceUuid) { + await MicroserviceLogStatusManager.delete( + { sessionId: sessionId }, + transaction + ) + } else if (session.fogUuid) { + await FogLogStatusManager.delete( + { sessionId: sessionId }, + transaction + ) + } + + // Trigger change tracking (so agent knows session is removed) + let fogUuid = session.fogUuid + if (!fogUuid && session.microserviceUuid) { + // Get fog UUID from microservice + const microservice = await MicroserviceManager.findOne( + { uuid: session.microserviceUuid }, + transaction + ) + if (microservice) { + fogUuid = microservice.iofogUuid + } + } + + if (fogUuid) { + const fog = await FogManager.findOne({ uuid: fogUuid }, transaction) + if (fog) { + await ChangeTrackingService.update( + fog.uuid, + session.microserviceUuid ? ChangeTrackingService.events.microserviceLogs : ChangeTrackingService.events.fogLogs, + transaction + ) + } + } + } catch (error) { + logger.error('Error removing log session from database:' + JSON.stringify({ + error: error.message, + stack: error.stack, + sessionId: sessionId, + microserviceUuid: session.microserviceUuid, + fogUuid: session.fogUuid + })) + } + } + + // Cleanup expired sessions (timeout mechanism) + async cleanupExpiredSessions (transaction) { + const now = Date.now() + const timeout = this.config.session.timeout || 3600000 // Default 1 hour + const expiredSessions = [] + + for (const [sessionId, session] of this.logSessions) { + const timeSinceLastActivity = now - session.lastActivity + const timeSinceCreation = now - session.createdAt + + // Session is expired if: + // 1. No activity for timeout period AND session is older than timeout + // 2. OR user disconnected but agent still connected (orphaned agent connection) + // 3. OR agent disconnected but user still connected (orphaned user connection) + const isExpired = ( + (timeSinceLastActivity > timeout && timeSinceCreation > timeout) || + (!session.user && session.agent) || // Orphaned agent + (!session.agent && session.user && timeSinceCreation > timeout) // Orphaned user (wait timeout before cleanup) + ) + + if (isExpired) { + expiredSessions.push(sessionId) + } + } + + // Remove expired sessions + for (const sessionId of expiredSessions) { + logger.info('Cleaning up expired log session:' + JSON.stringify({ sessionId })) + await this.removeLogSession(sessionId, transaction) + } + + return expiredSessions.length + } + + startCleanupInterval () { + const interval = this.config.session.cleanupInterval || 30000 // Default 30 seconds + this.cleanupInterval = setInterval(async () => { + try { + const models = require('../data/models') + const sequelize = models.sequelize + if (!sequelize) { + logger.warn('Sequelize not available, skipping log session cleanup') + return + } + + await sequelize.transaction(async (transaction) => { + await this.cleanupExpiredSessions(transaction) + }) + } catch (error) { + logger.error('Error during log session cleanup:' + JSON.stringify({ + error: error.message, + stack: error.stack + })) + } + }, interval) + } + + stopCleanupInterval () { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + this.cleanupInterval = null + } + } +} + +module.exports = LogSessionManager diff --git a/src/websocket/server.js b/src/websocket/server.js index 3ef2e88d..5534d03d 100644 --- a/src/websocket/server.js +++ b/src/websocket/server.js @@ -3,6 +3,7 @@ const config = require('../config') const logger = require('../logger') const Errors = require('../helpers/errors') const SessionManager = require('./session-manager') +const LogSessionManager = require('./log-session-manager') const { WebSocketError } = require('./error-handler') const MicroserviceManager = require('../data/managers/microservice-manager') const ApplicationManager = require('../data/managers/application-manager') @@ -14,6 +15,11 @@ const AuthDecorator = require('../decorators/authorization-decorator') const TransactionDecorator = require('../decorators/transaction-decorator') const msgpack = require('@msgpack/msgpack') const WebSocketQueueService = require('../services/websocket-queue-service') +const AppHelper = require('../helpers/app-helper') +const MicroserviceLogStatusManager = require('../data/managers/microservice-log-status-manager') +const FogLogStatusManager = require('../data/managers/fog-log-status-manager') +const ChangeTrackingService = require('../services/change-tracking-service') +const FogManager = require('../data/managers/iofog-manager') const MESSAGE_TYPES = { STDIN: 0, @@ -21,7 +27,11 @@ const MESSAGE_TYPES = { STDERR: 2, CONTROL: 3, CLOSE: 4, - ACTIVATION: 5 + ACTIVATION: 5, + LOG_LINE: 6, // Log line from agent + LOG_START: 7, // Log streaming started + LOG_STOP: 8, // Log streaming stopped + LOG_ERROR: 9 // Log streaming error } const EventService = require('../services/event-service') @@ -34,6 +44,7 @@ class WebSocketServer { this.connectionLimits = new Map() this.rateLimits = new Map() this.sessionManager = new SessionManager(config.get('server.webSocket')) + this.logSessionManager = new LogSessionManager(config.get('server.webSocket')) this.queueService = WebSocketQueueService this.pendingCloseTimeouts = new Map() // Track pending CLOSE messages in cross-replica scenarios this.config = { @@ -316,22 +327,34 @@ class WebSocketServer { return } - const microserviceUuid = this.extractMicroserviceUuid(req.url) - if (!microserviceUuid) { - logger.error('WebSocket connection failed: Invalid endpoint - no UUID found') - try { - ws.close(1008, 'Invalid endpoint') - } catch (error) { - logger.error('Error closing WebSocket:' + error.message) - } - return - } - // Determine connection type and handle accordingly if (req.url.startsWith('/api/v3/agent/exec/')) { + const microserviceUuid = this.extractMicroserviceUuid(req.url) + if (!microserviceUuid) { + logger.error('WebSocket connection failed: Invalid endpoint - no UUID found') + try { + ws.close(1008, 'Invalid endpoint') + } catch (error) { + logger.error('Error closing WebSocket:' + error.message) + } + return + } await this.handleAgentConnection(ws, req, token, microserviceUuid, transaction) } else if (req.url.startsWith('/api/v3/microservices/exec/')) { + const microserviceUuid = this.extractMicroserviceUuid(req.url) + if (!microserviceUuid) { + logger.error('WebSocket connection failed: Invalid endpoint - no UUID found') + try { + ws.close(1008, 'Invalid endpoint') + } catch (error) { + logger.error('Error closing WebSocket:' + error.message) + } + return + } await this.handleUserConnection(ws, req, token, microserviceUuid, transaction) + } else if (req.url.includes('/logs')) { + // Handle log connections (may not have microserviceUuid - could be fog logs) + await this.handleLogConnection(ws, req, token, transaction) } else { logger.error('WebSocket connection failed: Invalid endpoint') try { @@ -1638,6 +1661,61 @@ class WebSocketServer { } } + async validateAgentLogsConnection (token, microserviceUuid, iofogUuid, sessionId, transaction) { + try { + // 1. Validate agent token and get fog + let fog = {} + const req = { headers: { authorization: token }, transaction } + const handler = AuthDecorator.checkFogToken(async (req, fogObj) => { + fog = fogObj + return fogObj + }) + await handler(req) + + if (!fog) { + logger.error('Agent validation failed: Invalid agent token') + throw new WebSocketError(1008, 'Invalid agent token') + } + + // 2. Validate microservice or fog + if (microserviceUuid) { + // Verify microservice exists and belongs to this fog + const microservice = await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction) + if (!microservice || microservice.iofogUuid !== fog.uuid) { + logger.error('Agent validation failed: Microservice not found or not associated with this agent' + JSON.stringify({ + microserviceUuid, + fogUuid: fog.uuid, + found: !!microservice, + microserviceFogUuid: microservice ? microservice.iofogUuid : null + })) + throw new WebSocketError(1008, 'Microservice not found or not associated with this agent') + } + } else if (iofogUuid) { + // Verify fog UUID matches the authenticated fog + if (iofogUuid !== fog.uuid) { + logger.error('Agent validation failed: Fog UUID mismatch' + JSON.stringify({ + iofogUuid, + fogUuid: fog.uuid + })) + throw new WebSocketError(1008, 'Fog UUID mismatch') + } + } else { + throw new WebSocketError(1008, 'Either microserviceUuid or iofogUuid must be provided') + } + + return fog + } catch (error) { + logger.error('Agent logs validation error:' + JSON.stringify({ + error: error.message, + stack: error.stack, + microserviceUuid, + iofogUuid, + sessionId + })) + throw error // Propagate the original error + } + } + async validateUserConnection (token, microserviceUuid, transaction) { try { // 1. Authenticate user first (Keycloak) - Direct token verification @@ -1750,6 +1828,103 @@ class WebSocketServer { } } + async validateUserLogsConnection (token, microserviceUuid, fogUuid, transaction) { + try { + // 1. Authenticate user first (Keycloak) - Direct token verification + let userRoles = [] + + // Extract Bearer token + const bearerToken = token.replace('Bearer ', '') + if (!bearerToken) { + throw new Errors.AuthenticationError('Missing or invalid authorization token') + } + + // Check if we're in development mode (mock Keycloak) + const isDevMode = process.env.SERVER_DEV_MODE || config.get('server.devMode', true) + const hasAuthConfig = this.isAuthConfigured() + + if (!hasAuthConfig && isDevMode) { + // Use mock roles for development + userRoles = ['SRE', 'Developer', 'Viewer'] + logger.debug('Using mock authentication for development mode') + } else { + // Use real Keycloak token verification + try { + // Create a grant from the access token + const grant = await keycloak.grantManager.createGrant({ + access_token: bearerToken + }) + + // Extract roles from the token - get client-specific roles + const clientId = process.env.KC_CLIENT || config.get('auth.client.id') + const resourceAccess = grant.access_token.content.resource_access + + if (resourceAccess && resourceAccess[clientId] && resourceAccess[clientId].roles) { + userRoles = resourceAccess[clientId].roles + } else { + // Fallback to realm roles if client roles not found + userRoles = grant.access_token.content.realm_access && grant.access_token.content.realm_access.roles + ? grant.access_token.content.realm_access.roles + : [] + } + + logger.debug('Token verification successful, user roles:' + JSON.stringify(userRoles)) + } catch (keycloakError) { + logger.error('Keycloak token verification failed:' + JSON.stringify({ + error: keycloakError.message, + stack: keycloakError.stack + })) + throw new Errors.AuthenticationError('Invalid or expired token') + } + } + + // Check if user has required roles (SRE/Developer/Viewer for logs) + const hasRequiredRole = userRoles.some(role => ['SRE', 'Developer', 'Viewer'].includes(role)) + if (!hasRequiredRole) { + throw new Errors.AuthenticationError('Insufficient permissions. Required roles: SRE, Developer, or Viewer for log access') + } + + // 2. Validate microservice or fog + if (microserviceUuid) { + const microservice = await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction) + if (!microservice) { + throw new Errors.NotFoundError('Microservice not found') + } + + const application = await ApplicationManager.findOne({ id: microservice.applicationId }, transaction) + if (!application) { + throw new Errors.NotFoundError('Application not found') + } + + const statusArr = await MicroserviceStatusManager.findAllExcludeFields({ microserviceUuid: microserviceUuid }, transaction) + if (!statusArr || statusArr.length === 0) { + throw new Errors.NotFoundError('Microservice status not found') + } + const status = statusArr[0] + if (status.status !== microserviceState.RUNNING) { + throw new Errors.ValidationError('Microservice is not running') + } + + if (application.isSystem && !userRoles.includes('SRE')) { + throw new Errors.AuthenticationError('Only SRE can access system microservices') + } + } else if (fogUuid) { + const fog = await FogManager.findOne({ uuid: fogUuid }, transaction) + if (!fog) { + throw new Errors.NotFoundError('Fog not found') + } + // For fog logs, we can allow SRE/Developer/Viewer - no additional status check needed + } else { + throw new Errors.ValidationError('Either microserviceUuid or fogUuid must be provided') + } + + return { success: true } // Just indicate validation passed + } catch (error) { + logger.error('User logs connection validation failed:' + JSON.stringify({ error: error.message, stack: error.stack })) + throw error + } + } + // Singleton instance static getInstance () { if (!WebSocketServer.instance) { @@ -1959,6 +2134,710 @@ class WebSocketServer { return value !== undefined && value !== null && value !== '' }) } + + // Helper method to validate ISO 8601 format + isValidISO8601 (dateString) { + if (!dateString || typeof dateString !== 'string') { + return false + } + const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})$/ + if (!iso8601Regex.test(dateString)) { + return false + } + const date = new Date(dateString) + return date instanceof Date && !isNaN(date.getTime()) + } + + // Route log connections to appropriate handler + async handleLogConnection (ws, req, token, transaction) { + try { + const url = new URL(req.url, `http://${req.headers.host}`) + const pathParts = url.pathname.split('/').filter(p => p) + + // Check if this is an agent log connection (has sessionId in path) + if (pathParts.includes('agent') && pathParts.includes('logs')) { + // Extract microserviceUuid or iofogUuid and sessionId + let microserviceUuid = null + let iofogUuid = null + let sessionId = null + + if (pathParts.includes('microservice')) { + const microserviceIndex = pathParts.indexOf('microservice') + microserviceUuid = pathParts[microserviceIndex + 1] + sessionId = pathParts[microserviceIndex + 2] + } else if (pathParts.includes('iofog')) { + const iofogIndex = pathParts.indexOf('iofog') + iofogUuid = pathParts[iofogIndex + 1] + sessionId = pathParts[iofogIndex + 2] + } + + if (sessionId) { + await this.handleAgentLogsConnection(ws, req, token, microserviceUuid, iofogUuid, sessionId, transaction) + } else { + ws.close(1008, 'Missing sessionId in agent log connection') + } + } else { + // User log connection + let microserviceUuid = null + let fogUuid = null + + if (pathParts.includes('microservices')) { + const microserviceIndex = pathParts.indexOf('microservices') + microserviceUuid = pathParts[microserviceIndex + 1] + } else if (pathParts.includes('iofog')) { + const iofogIndex = pathParts.indexOf('iofog') + fogUuid = pathParts[iofogIndex + 1] + } + + await this.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, transaction) + } + } catch (error) { + logger.error('Error in handleLogConnection:', error) + if (ws.readyState === WebSocket.OPEN) { + ws.close(1008, error.message || 'Connection error') + } + } + } + + async handleUserLogsConnection (ws, req, token, microserviceUuid, fogUuid, transaction) { + try { + this.ensureSocketPongHandler(ws) + + // 1. Validate user authentication + await this.validateUserLogsConnection(token, microserviceUuid, fogUuid, transaction) + + // 2. Parse tail configuration from query parameters + const url = new URL(req.url, `http://${req.headers.host}`) + + // Parse and validate tail config + const tailLines = parseInt(url.searchParams.get('tail')) + const tailConfig = { + lines: (tailLines && tailLines >= 1 && tailLines <= 10000) ? tailLines : 100, // Default: 100, Range: 1-10000 + follow: url.searchParams.get('follow') !== 'false', // default: true + since: url.searchParams.get('since') || null, // ISO 8601 format + until: url.searchParams.get('until') || null // ISO 8601 format + } + + // Validate ISO 8601 format for since/until (if provided) + if (tailConfig.since && !this.isValidISO8601(tailConfig.since)) { + ws.close(1008, 'Invalid since format. Expected ISO 8601.') + return + } + if (tailConfig.until && !this.isValidISO8601(tailConfig.until)) { + ws.close(1008, 'Invalid until format. Expected ISO 8601.') + return + } + + // 3. Generate unique sessionId for this user session + const sessionId = AppHelper.generateUUID() + const logSessionId = fogUuid ? `logs-${fogUuid}` : `logs-${microserviceUuid}` + + // 4. Create log session in database (no HTTP POST needed!) + if (microserviceUuid) { + await MicroserviceLogStatusManager.create({ + microserviceUuid: microserviceUuid, + logSessionId: logSessionId, + sessionId: sessionId, // Unique per user session + status: 'PENDING', + tailConfig: JSON.stringify(tailConfig), + agentConnected: false, + userConnected: true + }, transaction) + } else if (fogUuid) { + await FogLogStatusManager.create({ + iofogUuid: fogUuid, + logSessionId: logSessionId, + sessionId: sessionId, // Unique per user session + status: 'PENDING', + tailConfig: JSON.stringify(tailConfig), + agentConnected: false, + userConnected: true + }, transaction) + } + + // 5. Trigger change tracking (notify agent of new session) + let fogUuidForTracking = fogUuid + if (!fogUuidForTracking && microserviceUuid) { + const microservice = await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction) + if (!microservice) { + throw new Error(`Microservice not found: ${microserviceUuid}`) + } + fogUuidForTracking = microservice.iofogUuid + } + + if (!fogUuidForTracking) { + throw new Error('Unable to determine fog UUID for change tracking') + } + + const fog = await FogManager.findOne({ + uuid: fogUuidForTracking + }, transaction) + + if (!fog) { + throw new Error(`Fog not found: ${fogUuidForTracking}`) + } + + await ChangeTrackingService.update( + fog.uuid, + fogUuid ? ChangeTrackingService.events.fogLogs : ChangeTrackingService.events.microserviceLogs, + transaction + ) + + logger.debug('Change tracking updated for log session:' + JSON.stringify({ + fogUuid: fog.uuid, + microserviceUuid, + eventType: microserviceUuid ? 'microserviceLogs' : 'fogLogs', + sessionId + })) + + // 6. Create in-memory session (one-to-one: user only, waiting for agent) + this.logSessionManager.createLogSession( + sessionId, + microserviceUuid, + fogUuid, + null, // Agent not connected yet + ws, // User connected + tailConfig, + transaction + ) + + // 7. Send sessionId to user (MessagePack encoded) + const sessionInfoMsg = { + type: MESSAGE_TYPES.LOG_START, + data: Buffer.from(JSON.stringify({ + sessionId: sessionId, + tailConfig: tailConfig + })), + sessionId: sessionId, + timestamp: Date.now() + } + ws.send(this.encodeMessage(sessionInfoMsg), { binary: true }) + + // 8. Send waiting message to user (agent not connected yet) + try { + const waitingMsg = { + type: MESSAGE_TYPES.LOG_LINE, + data: Buffer.from('Waiting for agent connection. Log streaming will begin once the agent connects.\n'), + sessionId: sessionId, + timestamp: Date.now(), + microserviceUuid: microserviceUuid || null, + iofogUuid: fogUuid || null + } + ws.send(this.encodeMessage(waitingMsg), { binary: true }) + logger.info('Sent waiting status message to user for log session:' + JSON.stringify({ + sessionId, + microserviceUuid, + fogUuid + })) + } catch (error) { + logger.warn('Failed to send waiting status message to user:' + JSON.stringify({ + error: error.message, + sessionId + })) + } + + // 9. Setup message forwarding (will be activated when agent connects) + await this.setupLogMessageForwarding(sessionId, transaction) + + // 10. Record WebSocket connection event (non-blocking) + setImmediate(async () => { + try { + // Extract actorId from token (req.kauth not available for WebSocket connections) + let actorId = null + if (req.headers && req.headers.authorization) { + actorId = EventService.extractUsernameFromToken(req.headers.authorization) + } + await EventService.createWsConnectEvent({ + timestamp: Date.now(), + endpointType: 'user', + actorId: actorId, + path: req.url, + resourceId: microserviceUuid || fogUuid, + ipAddress: EventService.extractIPv4Address(req) || null + }) + } catch (err) { + logger.error('Failed to create WS_CONNECT event for user log session (non-blocking):', err) + } + }) + + // Handle user disconnect + ws.on('close', async (code, reason) => { + const session = this.logSessionManager.getLogSession(sessionId) + if (session) { + session.user = null // Mark user as disconnected + session.lastActivity = Date.now() + + // Update database + if (microserviceUuid) { + await MicroserviceLogStatusManager.update( + { sessionId: sessionId }, + { userConnected: false }, + transaction + ) + } else if (fogUuid) { + await FogLogStatusManager.update( + { sessionId: sessionId }, + { userConnected: false }, + transaction + ) + } + + // If agent also disconnected, remove session + if (!session.agent) { + await this.logSessionManager.removeLogSession(sessionId, transaction) + } else { + // Trigger change tracking (agent will see user disconnected on next poll) + const fogForTracking = await FogManager.findOne({ + uuid: fogUuid || (await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction)).iofogUuid + }, transaction) + await ChangeTrackingService.update( + fogForTracking.uuid, + fogUuid ? ChangeTrackingService.events.fogLogs : ChangeTrackingService.events.microserviceLogs, + transaction + ) + } + } + + // Record WebSocket disconnection event (non-blocking) + setImmediate(async () => { + try { + // Extract actorId from token (req.kauth not available for WebSocket connections) + let actorId = null + if (req.headers && req.headers.authorization) { + actorId = EventService.extractUsernameFromToken(req.headers.authorization) + } + await EventService.createWsDisconnectEvent({ + timestamp: Date.now(), + endpointType: 'user', + actorId: actorId, + path: req.url, + resourceId: microserviceUuid || fogUuid, + ipAddress: EventService.extractIPv4Address(req) || null, + closeCode: code + }) + } catch (err) { + logger.error('Failed to create WS_DISCONNECT event for user log session (non-blocking):', err) + } + }) + }) + } catch (error) { + logger.error('User logs connection error:', error) + if (ws.readyState === WebSocket.OPEN) { + ws.close(1008, error.message) + } + } + } + + async handleAgentLogsConnection (ws, req, token, microserviceUuid, iofogUuid, sessionId, transaction) { + try { + this.ensureSocketPongHandler(ws) + + // 1. Validate agent token and resource (microservice or fog) + await this.validateAgentLogsConnection(token, microserviceUuid, iofogUuid, sessionId, transaction) + + // 2. Get session from database (by sessionId) + let logStatus = null + if (microserviceUuid) { + logStatus = await MicroserviceLogStatusManager.findOne( + { sessionId: sessionId }, + transaction + ) + } else if (iofogUuid) { + logStatus = await FogLogStatusManager.findOne( + { sessionId: sessionId }, + transaction + ) + } + + if (!logStatus) { + logger.error('Agent connected to non-existent session:', sessionId) + ws.close(1008, 'Session not found') + return + } + + // Validate sessionId belongs to correct resource + if (microserviceUuid && logStatus.microserviceUuid !== microserviceUuid) { + logger.error('Session does not belong to microservice:', { sessionId, microserviceUuid, logStatusMicroserviceUuid: logStatus.microserviceUuid }) + ws.close(1008, 'Session mismatch') + return + } + if (iofogUuid && logStatus.iofogUuid !== iofogUuid) { + logger.error('Session does not belong to fog:', { sessionId, iofogUuid, logStatusIofogUuid: logStatus.iofogUuid }) + ws.close(1008, 'Session mismatch') + return + } + + // 3. Parse tail config from database + const tailConfig = JSON.parse(logStatus.tailConfig) + + // 4. Update database + if (microserviceUuid) { + await MicroserviceLogStatusManager.update( + { sessionId: sessionId }, + { agentConnected: true, status: 'ACTIVE' }, + transaction + ) + } else if (iofogUuid) { + await FogLogStatusManager.update( + { sessionId: sessionId }, + { agentConnected: true, status: 'ACTIVE' }, + transaction + ) + } + + // 5. Get or create in-memory session + let session = this.logSessionManager.getLogSession(sessionId) + if (!session) { + // Session might be on different replica, create it + session = this.logSessionManager.createLogSession( + sessionId, + logStatus.microserviceUuid, + logStatus.iofogUuid, + ws, // Agent + null, // User (might be on different replica) + tailConfig, + transaction + ) + } else { + session.agent = ws + session.lastActivity = Date.now() + } + + // 5.5. Set up message handler IMMEDIATELY on the agent WebSocket + // This ensures messages are captured even if they arrive before setupLogMessageForwarding completes + // Critical for microservice logs which may have timing issues + ws.removeAllListeners('message') + ws.on('message', async (data, isBinary) => { + if (!isBinary) { + logger.warn('Received non-binary message from agent, expected MessagePack') + return + } + + // Decode MessagePack (same as exec sessions) + const buffer = Buffer.from(data) + let msg + try { + msg = this.decodeMessage(buffer) // MessagePack decode + } catch (error) { + logger.error('Failed to decode MessagePack from agent (direct handler):' + JSON.stringify({ + error: error.message, + sessionId, + bufferLength: buffer.length + })) + return + } + + logger.debug('Received log message from agent (direct handler):' + JSON.stringify({ + sessionId, + type: msg.type, + hasData: !!msg.data, + dataLength: msg.data ? msg.data.length : 0 + })) + + if (msg.type === MESSAGE_TYPES.LOG_LINE) { + // Forward to user (one-to-one, like exec sessions) + await this.forwardLogToUser(sessionId, buffer, transaction) + } else if (msg.type === MESSAGE_TYPES.LOG_START || + msg.type === MESSAGE_TYPES.LOG_STOP || + msg.type === MESSAGE_TYPES.LOG_ERROR) { + // Handle control messages + await this.forwardLogToUser(sessionId, buffer, transaction) + } + }) + + logger.debug('Set up direct message handler on agent WebSocket:' + JSON.stringify({ + sessionId, + microserviceUuid: logStatus.microserviceUuid, + iofogUuid: logStatus.iofogUuid, + agentState: ws.readyState + })) + + // 6. Send tail config to agent (so agent knows what to stream) + const configMsg = { + type: MESSAGE_TYPES.LOG_START, + data: Buffer.from(JSON.stringify({ + sessionId: sessionId, + tailConfig: tailConfig + })), + sessionId: sessionId, + timestamp: Date.now() + } + ws.send(this.encodeMessage(configMsg), { binary: true }) + + // 7. Notify user that agent has connected and streaming has started + if (session.user && session.user.readyState === WebSocket.OPEN) { + try { + const agentConnectedMsg = { + type: MESSAGE_TYPES.LOG_START, + data: Buffer.from(JSON.stringify({ + sessionId: sessionId, + message: 'Agent connected. Log streaming started.\n' + })), + sessionId: sessionId, + timestamp: Date.now() + } + session.user.send(this.encodeMessage(agentConnectedMsg), { binary: true }) + logger.info('Notified user that agent connected for log session:' + JSON.stringify({ + sessionId, + microserviceUuid: logStatus.microserviceUuid, + iofogUuid: logStatus.iofogUuid + })) + } catch (error) { + logger.warn('Failed to notify user that agent connected:' + JSON.stringify({ + error: error.message, + sessionId + })) + } + } + + // 8. Setup message forwarding (unidirectional: agent → user, one-to-one) + await this.setupLogMessageForwarding(sessionId, transaction) + + // 9. Record WebSocket connection event (non-blocking) + setImmediate(async () => { + try { + // Extract actorId from token (fog UUID from JWT sub field) + const authHeader = req.headers.authorization + let actorId = null + if (authHeader) { + const [scheme, token] = authHeader.split(' ') + if (scheme.toLowerCase() === 'bearer' && token) { + try { + const tokenParts = token.split('.') + if (tokenParts.length === 3) { + const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString()) + actorId = payload.sub || null + } + } catch (err) { + // Ignore token parsing errors + } + } + } + await EventService.createWsConnectEvent({ + timestamp: Date.now(), + endpointType: 'agent', + actorId: actorId, + path: req.url, + resourceId: microserviceUuid || iofogUuid, + ipAddress: EventService.extractIPv4Address(req) || null + }) + } catch (err) { + logger.error('Failed to create WS_CONNECT event for agent log session (non-blocking):', err) + } + }) + + // Handle agent disconnect + ws.on('close', async (code, reason) => { + const session = this.logSessionManager.getLogSession(sessionId) + if (session) { + session.agent = null // Mark agent as disconnected + session.lastActivity = Date.now() + + // Update database + if (microserviceUuid) { + await MicroserviceLogStatusManager.update( + { sessionId: sessionId }, + { agentConnected: false }, + transaction + ) + } else if (iofogUuid) { + await FogLogStatusManager.update( + { sessionId: sessionId }, + { agentConnected: false }, + transaction + ) + } + + // If user also disconnected, remove session + if (!session.user) { + await this.logSessionManager.removeLogSession(sessionId, transaction) + } else { + // Trigger change tracking (agent will see it disconnected on next poll) + const fog = await FogManager.findOne({ + uuid: iofogUuid || logStatus.iofogUuid || (await MicroserviceManager.findOne({ uuid: logStatus.microserviceUuid }, transaction)).iofogUuid + }, transaction) + await ChangeTrackingService.update( + fog.uuid, + iofogUuid ? ChangeTrackingService.events.fogLogs : ChangeTrackingService.events.microserviceLogs, + transaction + ) + } + } + + // Record WebSocket disconnection event (non-blocking) + setImmediate(async () => { + try { + // Extract actorId from token (fog UUID from JWT sub field) + const authHeader = req.headers.authorization + let actorId = null + if (authHeader) { + const [scheme, token] = authHeader.split(' ') + if (scheme.toLowerCase() === 'bearer' && token) { + try { + const tokenParts = token.split('.') + if (tokenParts.length === 3) { + const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString()) + actorId = payload.sub || null + } + } catch (err) { + // Ignore token parsing errors + } + } + } + await EventService.createWsDisconnectEvent({ + timestamp: Date.now(), + endpointType: 'agent', + actorId: actorId, + path: req.url, + resourceId: microserviceUuid || iofogUuid, + ipAddress: EventService.extractIPv4Address(req) || null, + closeCode: code + }) + } catch (err) { + logger.error('Failed to create WS_DISCONNECT event for agent log session (non-blocking):', err) + } + }) + }) + } catch (error) { + logger.error('Agent logs connection error:', error) + if (ws.readyState === WebSocket.OPEN) { + ws.close(1008, error.message) + } + } + } + + async setupLogMessageForwarding (sessionId, transaction) { + const session = this.logSessionManager.getLogSession(sessionId) + if (!session) { + logger.warn('setupLogMessageForwarding: Session not found:' + JSON.stringify({ sessionId })) + return + } + + // Enable queue bridge for cross-replica support (one-to-one, like exec sessions) + await this.queueService.enableForLogSession(session, (sessionId) => { + this.cleanupLogSession(sessionId, transaction) + }) + + // ONLY agent → user forwarding (unidirectional, one-to-one) + // All messages from agent are MessagePack encoded (binary) + if (session.agent) { + // Remove any existing message handlers to avoid duplicates (like exec sessions) + session.agent.removeAllListeners('message') + + logger.debug('Setting up agent message handler for log session:' + JSON.stringify({ + sessionId, + microserviceUuid: session.microserviceUuid, + fogUuid: session.fogUuid, + agentState: session.agent.readyState, + userState: session.user ? session.user.readyState : 'N/A' + })) + + session.agent.on('message', async (data, isBinary) => { + if (!isBinary) { + logger.warn('Received non-binary message from agent, expected MessagePack') + return + } + + // Decode MessagePack (same as exec sessions) + const buffer = Buffer.from(data) + let msg + try { + msg = this.decodeMessage(buffer) // MessagePack decode + } catch (error) { + logger.error('Failed to decode MessagePack from agent:' + JSON.stringify({ + error: error.message, + sessionId, + bufferLength: buffer.length + })) + return + } + + logger.debug('Received log message from agent:' + JSON.stringify({ + sessionId, + type: msg.type, + hasData: !!msg.data, + dataLength: msg.data ? msg.data.length : 0 + })) + + if (msg.type === MESSAGE_TYPES.LOG_LINE) { + // Forward to user (one-to-one, like exec sessions) + await this.forwardLogToUser(sessionId, buffer, transaction) + } else if (msg.type === MESSAGE_TYPES.LOG_START || + msg.type === MESSAGE_TYPES.LOG_STOP || + msg.type === MESSAGE_TYPES.LOG_ERROR) { + // Handle control messages + await this.forwardLogToUser(sessionId, buffer, transaction) + } + }) + } else { + logger.debug('setupLogMessageForwarding: Agent not connected yet:' + JSON.stringify({ + sessionId, + microserviceUuid: session.microserviceUuid, + fogUuid: session.fogUuid + })) + } + + // NO user → agent forwarding needed! + // Users are read-only + } + + async forwardLogToUser (sessionId, buffer, transaction) { + const session = this.logSessionManager.getLogSession(sessionId) + if (!session) { + logger.warn('forwardLogToUser: Session not found:' + JSON.stringify({ sessionId })) + return + } + + // Buffer is already MessagePack encoded from agent + // Following exec session pattern: Use queue for ALL scenarios (single and multi-replica) + // One-to-one forwarding (agent → user) via queue + const useQueue = this.queueService.shouldUseQueueForLogs(sessionId) + logger.debug('forwardLogToUser:' + JSON.stringify({ + sessionId, + useQueue, + hasUser: !!session.user, + userState: session.user ? session.user.readyState : 'N/A', + bufferLength: buffer.length + })) + + if (useQueue) { + // Publish MessagePack encoded buffer to user queue + await this.queueService.publishLogToUser(sessionId, buffer) + } else { + // Fallback: Direct WebSocket (only if queue not enabled) + // Send MessagePack encoded buffer directly (binary) + if (session.user && session.user.readyState === WebSocket.OPEN) { + try { + session.user.send(buffer, { + binary: true, // MessagePack is binary + compress: false, + mask: false, + fin: true + }) + logger.debug('Sent log message directly to user:' + JSON.stringify({ + sessionId, + bufferLength: buffer.length + })) + } catch (error) { + logger.error('Failed to send log to user:' + JSON.stringify({ + error: error.message, + sessionId, + bufferLength: buffer.length + })) + } + } else { + logger.warn('Cannot send log to user - user not connected:' + JSON.stringify({ + sessionId, + userState: session.user ? session.user.readyState : 'N/A' + })) + } + } + } + + async cleanupLogSession (sessionId, transaction) { + await this.logSessionManager.removeLogSession(sessionId, transaction) + await this.queueService.cleanupLogSession(sessionId) + } } module.exports = WebSocketServer From 81015dd939ef35351b8e9e1af69b5a24f7eef064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Wed, 4 Feb 2026 18:22:04 +0300 Subject: [PATCH 3/4] Add RBAC (roles, role bindings, service accounts) and cluster controller support with authorizer/middleware, new routes and services, and DB migrations v1.1.0. Rename controller.yaml to config.yaml and wire RBAC plus cluster controller through existing routes and the websocket server. --- .nsprc | 6 +- docs/swagger.yaml | 1399 +++++++++++++++-- package-lock.json | 1161 +++++++------- package.json | 8 +- src/config/{controller.yaml => config.yaml} | 6 +- src/config/env-mapping.js | 6 +- src/config/index.js | 2 +- src/config/rbac-resources.yaml | 759 +++++++++ src/config/rbac-system-roles.js | 157 ++ src/controllers/cluster-controller.js | 41 + src/controllers/rbac-controller.js | 160 ++ src/controllers/registry-controller.js | 5 + .../managers/cluster-controller-manager.js | 23 + src/data/managers/microservice-manager.js | 31 + .../managers/rbac-cache-version-manager.js | 70 + .../managers/rbac-role-binding-manager.js | 181 +++ src/data/managers/rbac-role-manager.js | 293 ++++ .../managers/rbac-service-account-manager.js | 162 ++ src/data/managers/registry-manager.js | 14 + ....0.7.sql => db_migration_mysql_v1.1.0.sql} | 81 + ..._v1.0.7.sql => db_migration_pg_v1.1.0.sql} | 76 + ...0.7.sql => db_migration_sqlite_v1.1.0.sql} | 75 + src/data/models/cluster-controller.js | 37 + src/data/models/index.js | 20 + src/data/models/microservice.js | 13 + src/data/models/rbacCacheVersion.js | 31 + src/data/models/rbacRole.js | 48 + src/data/models/rbacRoleBinding.js | 86 + src/data/models/rbacRoleRule.js | 112 ++ src/data/models/rbacServiceAccount.js | 67 + src/data/models/registry.js | 82 +- src/data/providers/database-provider.js | 12 +- src/helpers/error-messages.js | 3 + src/jobs/controller-cleanup-job.js | 71 + src/jobs/controller-heartbeat-job.js | 49 + src/lib/rbac/authorizer.js | 264 ++++ src/lib/rbac/middleware.js | 561 +++++++ src/routes/agent.js | 36 +- src/routes/application.js | 66 +- src/routes/applicationTemplate.js | 48 +- src/routes/capabilities.js | 10 +- src/routes/catalog.js | 30 +- src/routes/certificate.js | 44 +- src/routes/cluster.js | 145 ++ src/routes/config.js | 18 +- src/routes/configMap.js | 28 +- src/routes/diagnostics.js | 30 +- src/routes/edgeResource.js | 48 +- src/routes/event.js | 8 +- src/routes/flow.js | 30 +- src/routes/iofog.js | 80 +- src/routes/microservices.js | 305 ++-- src/routes/rbac.js | 697 ++++++++ src/routes/registries.js | 51 +- src/routes/router.js | 8 +- src/routes/routing.js | 20 +- src/routes/secret.js | 28 +- src/routes/service.js | 28 +- src/routes/tunnel.js | 8 +- src/routes/volumeMount.js | 60 +- src/schemas/cluster-controller.js | 28 + src/schemas/microservice.js | 30 + src/schemas/rbac.js | 248 +++ src/server.js | 10 +- src/services/agent-service.js | 54 +- src/services/cluster-controller-service.js | 169 ++ src/services/event-service.js | 1 + src/services/microservices-service.js | 145 ++ src/services/rbac-service.js | 292 ++++ src/services/registry-service.js | 34 +- src/services/router-service.js | 131 +- src/services/services-service.js | 89 +- src/services/yaml-parser-service.js | 82 +- src/websocket/server.js | 640 +++++--- 74 files changed, 8496 insertions(+), 1455 deletions(-) rename src/config/{controller.yaml => config.yaml} (95%) create mode 100644 src/config/rbac-resources.yaml create mode 100644 src/config/rbac-system-roles.js create mode 100644 src/controllers/cluster-controller.js create mode 100644 src/controllers/rbac-controller.js create mode 100644 src/data/managers/cluster-controller-manager.js create mode 100644 src/data/managers/rbac-cache-version-manager.js create mode 100644 src/data/managers/rbac-role-binding-manager.js create mode 100644 src/data/managers/rbac-role-manager.js create mode 100644 src/data/managers/rbac-service-account-manager.js rename src/data/migrations/mysql/{db_migration_mysql_v1.0.7.sql => db_migration_mysql_v1.1.0.sql} (91%) rename src/data/migrations/postgres/{db_migration_pg_v1.0.7.sql => db_migration_pg_v1.1.0.sql} (91%) rename src/data/migrations/sqlite/{db_migration_sqlite_v1.0.7.sql => db_migration_sqlite_v1.1.0.sql} (92%) create mode 100644 src/data/models/cluster-controller.js create mode 100644 src/data/models/rbacCacheVersion.js create mode 100644 src/data/models/rbacRole.js create mode 100644 src/data/models/rbacRoleBinding.js create mode 100644 src/data/models/rbacRoleRule.js create mode 100644 src/data/models/rbacServiceAccount.js create mode 100644 src/jobs/controller-cleanup-job.js create mode 100644 src/jobs/controller-heartbeat-job.js create mode 100644 src/lib/rbac/authorizer.js create mode 100644 src/lib/rbac/middleware.js create mode 100644 src/routes/cluster.js create mode 100644 src/routes/rbac.js create mode 100644 src/schemas/cluster-controller.js create mode 100644 src/schemas/rbac.js create mode 100644 src/services/cluster-controller-service.js create mode 100644 src/services/rbac-service.js diff --git a/.nsprc b/.nsprc index 9c2174be..ace7fe1d 100644 --- a/.nsprc +++ b/.nsprc @@ -1,3 +1,5 @@ { - - } \ No newline at end of file + "1112030": { + "notes": "" + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2c62aac3..ca2398f6 100755 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,6 +1,6 @@ openapi : "3.0.0" info: - version: 3.5.10 + version: 3.6.0 title: Datasance PoT Controller paths: /status: @@ -672,6 +672,131 @@ paths: description: Not Found "500": description: Internal Server Error + /cluster/controllers: + get: + tags: + - Cluster + summary: Lists all cluster controllers + operationId: listClusterControllers + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ClusterControllerListResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "500": + description: Internal Server Error + /cluster/controllers/{uuid}: + get: + tags: + - Cluster + summary: Gets a cluster controller details + operationId: getClusterController + parameters: + - in: path + name: uuid + description: Cluster controller UUID + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ClusterControllerResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + patch: + tags: + - Cluster + summary: Updates a cluster controller + operationId: updateClusterController + parameters: + - in: path + name: uuid + description: Cluster controller UUID + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ClusterControllerUpdateRequest" + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ClusterControllerResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + delete: + tags: + - Cluster + summary: Deletes a cluster controller + operationId: deleteClusterController + parameters: + - in: path + name: uuid + description: Cluster controller UUID + required: true + schema: + type: string + security: + - authToken: [] + responses: + "204": + description: No Content + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error /applicationTemplates: get: tags: @@ -3182,6 +3307,38 @@ paths: description: Invalid Registry Id "500": description: Internal Server Error + get: + tags: + - Registries + summary: Gets a registry + operationId: getRegistry + parameters: + - in: path + name: id + description: Registry id + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RegistryResponse" + "401": + description: Not Authorized + "404": + description: Invalid Registry Id + "500": + description: Internal Server Error /user/login: post: tags: @@ -5052,129 +5209,858 @@ paths: description: Not Authorized "500": description: Internal Server Error -tags: - - name: Controller - description: Manage your controller - - name: ioFog - description: Manage your agents - - name: Application - description: Manage your applications - - name: Application Template - description: Manage your application templates - - name: Catalog - description: Manage your catalog - - name: Registries - description: Manage your registries - - name: Microservices - description: Manage your microservices - - name: Routing - description: Manage your routes - - name: Edge Resource - description: Manage your Edge Resources - - name: Diagnostics - description: Diagnostic your microservices - - name: Tunnel - description: Manage ssh tunnels - - name: Agent - description: Used by your agents to communicate with your controller - - name: User - description: Manage your users - - name: Secrets - description: Manage your secrets - - name: Certificates - description: Manage your certificates - - name: Services - description: Manage your services - - name: VolumeMounts - description: Manage your volume mounts - - name: ConfigMap - description: Manage your config maps - - name: Events - description: Manage audit events -servers: - - url: http://localhost:51121/api/v3 -components: - securitySchemes: - authToken: - type: http - scheme: bearer - bearerFormat: JWT - requestBodies: - UpdateIOFogNodeRequestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/UpdateIOFogNodeRequestBody" - required: true - CreateUpdateCatalogItemRequestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateUpdateCatalogItemRequestBody" - description: Microservice Catalog Item Info - required: true - HalInfo: - content: - application/json: - schema: - $ref: "#/components/schemas/HalInfo" - required: true - ApplicationTemplateCreateRequest: - content: - application/json: - schema: - $ref: "#/components/schemas/ApplicationTemplateCreateRequest" - required: true - NewFlowRequest: - content: - application/json: - schema: - $ref: "#/components/schemas/NewFlowRequest" - description: New Flow Info - required: true - schemas: - EventRecord: - type: object - properties: - id: - type: integer - timestamp: - type: integer - format: int64 - eventType: - type: string - endpointType: - type: string - actorId: - type: string - nullable: true - method: - type: string - nullable: true - resourceType: - type: string - nullable: true - resourceId: - type: string - nullable: true - endpointPath: - type: string - ipAddress: - type: string - nullable: true - status: - type: string - statusCode: - type: integer - nullable: true - statusMessage: - type: string - nullable: true - requestId: - type: string - nullable: true - createdAt: - type: string + /roles: + get: + tags: + - Roles + summary: Lists all roles + operationId: listRoles + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleListResponse" + "401": + description: Not Authorized + "500": + description: Internal Server Error + post: + tags: + - Roles + summary: Creates a new role + operationId: createRole + security: + - authToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoleCreateRequest" + responses: + "201": + description: Created + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "409": + description: Role Already Exists + "500": + description: Internal Server Error + /roles/yaml: + post: + tags: + - Roles + summary: Create a role from YAML file + operationId: createRoleFromYAML + security: + - authToken: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + role: + type: string + format: binary + responses: + "201": + description: Created + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "409": + description: Role Already Exists + "500": + description: Internal Server Error + "/roles/{name}": + get: + tags: + - Roles + summary: Gets a role by name + operationId: getRole + parameters: + - in: path + name: name + description: Role name + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleResponse" + "401": + description: Not Authorized + "404": + description: Role Not Found + "500": + description: Internal Server Error + patch: + tags: + - Roles + summary: Updates an existing role + operationId: updateRole + parameters: + - in: path + name: name + description: Role name + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoleCreateRequest" + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Role Not Found + "500": + description: Internal Server Error + delete: + tags: + - Roles + summary: Deletes a role + operationId: deleteRole + parameters: + - in: path + name: name + description: Role name + required: true + schema: + type: string + security: + - authToken: [] + responses: + "204": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "401": + description: Not Authorized + "404": + description: Role Not Found + "500": + description: Internal Server Error + "/roles/yaml/{name}": + patch: + tags: + - Roles + summary: Updates an existing role using YAML + operationId: updateRoleFromYAML + parameters: + - in: path + name: name + description: Role name + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + role: + type: string + format: binary + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Role Not Found + "500": + description: Internal Server Error + /rolebindings: + get: + tags: + - RoleBindings + summary: Lists all role bindings + operationId: listRoleBindings + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingListResponse" + "401": + description: Not Authorized + "500": + description: Internal Server Error + post: + tags: + - RoleBindings + summary: Creates a new role binding + operationId: createRoleBinding + security: + - authToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingCreateRequest" + responses: + "201": + description: Created + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "409": + description: RoleBinding Already Exists + "500": + description: Internal Server Error + /rolebindings/yaml: + post: + tags: + - RoleBindings + summary: Create a role binding from YAML file + operationId: createRoleBindingFromYAML + security: + - authToken: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + rolebinding: + type: string + format: binary + responses: + "201": + description: Created + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "409": + description: RoleBinding Already Exists + "500": + description: Internal Server Error + "/rolebindings/{name}": + get: + tags: + - RoleBindings + summary: Gets a role binding by name + operationId: getRoleBinding + parameters: + - in: path + name: name + description: RoleBinding name + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingResponse" + "401": + description: Not Authorized + "404": + description: RoleBinding Not Found + "500": + description: Internal Server Error + patch: + tags: + - RoleBindings + summary: Updates an existing role binding + operationId: updateRoleBinding + parameters: + - in: path + name: name + description: RoleBinding name + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingCreateRequest" + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: RoleBinding Not Found + "500": + description: Internal Server Error + delete: + tags: + - RoleBindings + summary: Deletes a role binding + operationId: deleteRoleBinding + parameters: + - in: path + name: name + description: RoleBinding name + required: true + schema: + type: string + security: + - authToken: [] + responses: + "204": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "401": + description: Not Authorized + "404": + description: RoleBinding Not Found + "500": + description: Internal Server Error + "/rolebindings/yaml/{name}": + patch: + tags: + - RoleBindings + summary: Updates an existing role binding using YAML + operationId: updateRoleBindingFromYAML + parameters: + - in: path + name: name + description: RoleBinding name + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + rolebinding: + type: string + format: binary + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/RoleBindingResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: RoleBinding Not Found + "500": + description: Internal Server Error + /serviceaccounts: + get: + tags: + - ServiceAccounts + summary: Lists all service accounts + operationId: listServiceAccounts + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountListResponse" + "401": + description: Not Authorized + "500": + description: Internal Server Error + post: + tags: + - ServiceAccounts + summary: Creates a new service account + operationId: createServiceAccount + security: + - authToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountCreateRequest" + responses: + "201": + description: Created + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "409": + description: ServiceAccount Already Exists + "500": + description: Internal Server Error + /serviceaccounts/yaml: + post: + tags: + - ServiceAccounts + summary: Create a service account from YAML file + operationId: createServiceAccountFromYAML + security: + - authToken: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + serviceaccount: + type: string + format: binary + responses: + "201": + description: Created + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "409": + description: ServiceAccount Already Exists + "500": + description: Internal Server Error + "/serviceaccounts/{name}": + get: + tags: + - ServiceAccounts + summary: Gets a service account by name + operationId: getServiceAccount + parameters: + - in: path + name: name + description: ServiceAccount name + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountResponse" + "401": + description: Not Authorized + "404": + description: ServiceAccount Not Found + "500": + description: Internal Server Error + patch: + tags: + - ServiceAccounts + summary: Updates an existing service account + operationId: updateServiceAccount + parameters: + - in: path + name: name + description: ServiceAccount name + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountCreateRequest" + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: ServiceAccount Not Found + "500": + description: Internal Server Error + delete: + tags: + - ServiceAccounts + summary: Deletes a service account + operationId: deleteServiceAccount + parameters: + - in: path + name: name + description: ServiceAccount name + required: true + schema: + type: string + security: + - authToken: [] + responses: + "204": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "401": + description: Not Authorized + "404": + description: ServiceAccount Not Found + "500": + description: Internal Server Error + "/serviceaccounts/yaml/{name}": + patch: + tags: + - ServiceAccounts + summary: Updates an existing service account using YAML + operationId: updateServiceAccountFromYAML + parameters: + - in: path + name: name + description: ServiceAccount name + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + serviceaccount: + type: string + format: binary + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceAccountResponse" + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: ServiceAccount Not Found + "500": + description: Internal Server Error +tags: + - name: Controller + description: Manage your controller + - name: ioFog + description: Manage your agents + - name: Application + description: Manage your applications + - name: Application Template + description: Manage your application templates + - name: Catalog + description: Manage your catalog + - name: Registries + description: Manage your registries + - name: Microservices + description: Manage your microservices + - name: Routing + description: Manage your routes + - name: Edge Resource + description: Manage your Edge Resources + - name: Diagnostics + description: Diagnostic your microservices + - name: Tunnel + description: Manage ssh tunnels + - name: Agent + description: Used by your agents to communicate with your controller + - name: User + description: Manage your users + - name: Secrets + description: Manage your secrets + - name: Certificates + description: Manage your certificates + - name: Services + description: Manage your services + - name: VolumeMounts + description: Manage your volume mounts + - name: ConfigMap + description: Manage your config maps + - name: Events + description: Manage audit events + - name: Roles + description: Manage RBAC roles + - name: RoleBindings + description: Manage RBAC role bindings + - name: ServiceAccounts + description: Manage RBAC service accounts +servers: + - url: http://localhost:51121/api/v3 +components: + securitySchemes: + authToken: + type: http + scheme: bearer + bearerFormat: JWT + requestBodies: + UpdateIOFogNodeRequestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateIOFogNodeRequestBody" + required: true + CreateUpdateCatalogItemRequestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUpdateCatalogItemRequestBody" + description: Microservice Catalog Item Info + required: true + HalInfo: + content: + application/json: + schema: + $ref: "#/components/schemas/HalInfo" + required: true + ApplicationTemplateCreateRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/ApplicationTemplateCreateRequest" + required: true + NewFlowRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/NewFlowRequest" + description: New Flow Info + required: true + schemas: + EventRecord: + type: object + properties: + id: + type: integer + timestamp: + type: integer + format: int64 + eventType: + type: string + endpointType: + type: string + actorId: + type: string + nullable: true + method: + type: string + nullable: true + resourceType: + type: string + nullable: true + resourceId: + type: string + nullable: true + endpointPath: + type: string + ipAddress: + type: string + nullable: true + status: + type: string + statusCode: + type: integer + nullable: true + statusMessage: + type: string + nullable: true + requestId: + type: string + nullable: true + createdAt: + type: string format: date-time updatedAt: type: string @@ -6728,6 +7614,12 @@ components: type: array items: type: string + serviceAccount: + type: object + properties: + roleRef: + $ref: "#/components/schemas/RoleRef" + additionalProperties: false UpdateMicroserviceRequest: type: object required: @@ -6793,6 +7685,12 @@ components: type: array items: type: string + serviceAccount: + type: object + properties: + roleRef: + $ref: "#/components/schemas/RoleRef" + additionalProperties: false IOFogNodeTunnelStatusInfoResponse: type: object properties: @@ -7430,4 +8328,255 @@ components: type: string minItems: 1 required: - - fogUuids \ No newline at end of file + - fogUuids + RbacRule: + type: object + properties: + apiGroups: + type: array + items: + type: string + resources: + type: array + items: + type: string + verbs: + type: array + items: + type: string + resourceNames: + type: array + items: + type: string + nullable: true + required: + - apiGroups + - resources + - verbs + RoleRef: + type: object + required: + - kind + - name + properties: + kind: + type: string + name: + type: string + apiGroup: + type: string + Subject: + type: object + required: + - kind + - name + properties: + kind: + type: string + name: + type: string + apiGroup: + type: string + Role: + type: object + properties: + id: + type: integer + name: + type: string + apiVersion: + type: string + kind: + type: string + rules: + type: array + items: + $ref: "#/components/schemas/RbacRule" + required: + - name + - apiVersion + - kind + - rules + RoleCreateRequest: + type: object + properties: + name: + type: string + apiVersion: + type: string + kind: + type: string + rules: + type: array + items: + $ref: "#/components/schemas/RbacRule" + required: + - name + - rules + RoleResponse: + type: object + properties: + id: + type: integer + name: + type: string + apiVersion: + type: string + kind: + type: string + rules: + type: array + items: + $ref: "#/components/schemas/RbacRule" + required: + - name + - apiVersion + - kind + - rules + RoleListResponse: + type: array + items: + $ref: "#/components/schemas/RoleResponse" + RoleBinding: + type: object + properties: + id: + type: integer + name: + type: string + apiVersion: + type: string + kind: + type: string + roleRef: + $ref: "#/components/schemas/RoleRef" + subjects: + type: array + items: + $ref: "#/components/schemas/Subject" + required: + - name + - apiVersion + - kind + - roleRef + - subjects + RoleBindingCreateRequest: + type: object + properties: + name: + type: string + apiVersion: + type: string + kind: + type: string + roleRef: + $ref: "#/components/schemas/RoleRef" + subjects: + type: array + items: + $ref: "#/components/schemas/Subject" + required: + - name + - roleRef + - subjects + RoleBindingResponse: + type: object + properties: + id: + type: integer + name: + type: string + apiVersion: + type: string + kind: + type: string + roleRef: + $ref: "#/components/schemas/RoleRef" + subjects: + type: array + items: + $ref: "#/components/schemas/Subject" + required: + - name + - apiVersion + - kind + - roleRef + - subjects + RoleBindingListResponse: + type: array + items: + $ref: "#/components/schemas/RoleBindingResponse" + ServiceAccount: + type: object + properties: + id: + type: integer + name: + type: string + roleRef: + $ref: "#/components/schemas/RoleRef" + required: + - name + - roleRef + ServiceAccountCreateRequest: + type: object + properties: + name: + type: string + roleRef: + $ref: "#/components/schemas/RoleRef" + required: + - name + - roleRef + ServiceAccountResponse: + type: object + properties: + id: + type: integer + name: + type: string + roleRef: + $ref: "#/components/schemas/RoleRef" + required: + - name + - roleRef + ServiceAccountListResponse: + type: array + items: + $ref: "#/components/schemas/ServiceAccountResponse" + ClusterControllerResponse: + type: object + properties: + uuid: + type: string + host: + type: string + nullable: true + processId: + type: integer + nullable: true + lastHeartbeat: + type: string + format: date-time + nullable: true + isActive: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - uuid + - isActive + ClusterControllerListResponse: + type: array + items: + $ref: "#/components/schemas/ClusterControllerResponse" + ClusterControllerUpdateRequest: + type: object + properties: + host: + type: string + additionalProperties: false \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6a46bcae..242f75c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@datasance/iofogcontroller", - "version": "3.5.12", + "version": "3.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@datasance/iofogcontroller", - "version": "3.5.12", + "version": "3.6.0", "hasInstallScript": true, "license": "EPL-2.0", "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.948.0", + "@aws-sdk/client-secrets-manager": "^3.981.0", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@datasance/ecn-viewer": "1.2.6", @@ -203,476 +203,521 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.948.0.tgz", - "integrity": "sha512-pvGhKhTAPgRN2Nevpj7pywDMh2TuVjA/DCzbybhHyk3cb7Woz3gRW7thvMqaRo/Lnb4SsP6Bkx8P6STb5mXtcA==", + "version": "3.981.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.981.0.tgz", + "integrity": "sha512-NVSbeeU/IjVobvFrwR4vLaEn3L83SfqRZXjIyBlHtU6agtHVCOJCdhrkK0z7uFahxD9FqqiQTYMYhzIfgL7VjA==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-node": "3.948.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.981.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", - "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz", + "integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", + "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "version": "3.973.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz", + "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.2", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", - "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz", + "integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", - "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz", + "integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", - "integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-login": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz", + "integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-env": "^3.972.3", + "@aws-sdk/credential-provider-http": "^3.972.5", + "@aws-sdk/credential-provider-login": "^3.972.3", + "@aws-sdk/credential-provider-process": "^3.972.3", + "@aws-sdk/credential-provider-sso": "^3.972.3", + "@aws-sdk/credential-provider-web-identity": "^3.972.3", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", - "integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz", + "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz", - "integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-ini": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz", + "integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.3", + "@aws-sdk/credential-provider-http": "^3.972.5", + "@aws-sdk/credential-provider-ini": "^3.972.3", + "@aws-sdk/credential-provider-process": "^3.972.3", + "@aws-sdk/credential-provider-sso": "^3.972.3", + "@aws-sdk/credential-provider-web-identity": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", - "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz", + "integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", - "integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==", - "dependencies": { - "@aws-sdk/client-sso": "3.948.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/token-providers": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz", + "integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==", + "dependencies": { + "@aws-sdk/client-sso": "3.980.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/token-providers": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", - "integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz", + "integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", - "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", + "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", - "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", + "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", - "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", + "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", "dependencies": { - "@aws-sdk/types": "3.936.0", + "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", - "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.7", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz", + "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@smithy/core": "^3.22.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", + "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", - "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz", + "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", + "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", - "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", + "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", - "integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz", + "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==", + "dependencies": { + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", - "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", - "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-endpoints": "^3.2.5", + "version": "3.981.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.981.0.tgz", + "integrity": "sha512-a8nXh/H3/4j+sxhZk+N3acSDlgwTVSZbX9i55dx41gI1H+geuonuRG+Shv3GZsCb46vzc08RK2qC78ypO8uRlg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", - "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "version": "3.965.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz", + "integrity": "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", - "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", + "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", - "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz", + "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" @@ -684,22 +729,22 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", - "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz", + "integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==", "dependencies": { - "@smithy/types": "^4.9.0", - "fast-xml-parser": "5.2.5", + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", - "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", "engines": { "node": ">=18.0.0" } @@ -2914,11 +2959,11 @@ "dev": true }, "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", + "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -2926,15 +2971,15 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", - "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", + "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -2942,17 +2987,17 @@ } }, "node_modules/@smithy/core": { - "version": "3.18.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", - "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", + "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -2962,14 +3007,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", + "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -2977,13 +3022,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", + "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -2992,11 +3037,11 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", - "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", + "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -3006,11 +3051,11 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", - "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", + "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3029,12 +3074,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", - "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", + "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3042,17 +3087,17 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.14.tgz", - "integrity": "sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==", - "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-middleware": "^4.2.5", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", + "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", + "dependencies": { + "@smithy/core": "^3.22.1", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -3060,17 +3105,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.14.tgz", - "integrity": "sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==", - "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/service-error-classification": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", + "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -3079,12 +3124,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", - "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", + "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3092,11 +3137,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", - "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", + "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3104,13 +3149,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", - "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", + "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3118,14 +3163,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", - "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", + "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3133,11 +3178,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", - "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", + "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3145,11 +3190,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", - "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3157,11 +3202,11 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", + "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -3170,11 +3215,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", - "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", + "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3182,22 +3227,22 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", - "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", + "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "dependencies": { - "@smithy/types": "^4.9.0" + "@smithy/types": "^4.12.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", - "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", + "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3205,15 +3250,15 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -3223,16 +3268,16 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.9.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.10.tgz", - "integrity": "sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==", - "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", + "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", + "dependencies": { + "@smithy/core": "^3.22.1", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" }, "engines": { @@ -3240,9 +3285,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", + "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "dependencies": { "tslib": "^2.6.2" }, @@ -3251,12 +3296,12 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", - "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", + "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "dependencies": { - "@smithy/querystring-parser": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3322,13 +3367,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.13.tgz", - "integrity": "sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==", + "version": "4.3.29", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", + "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3336,16 +3381,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.16.tgz", - "integrity": "sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==", - "dependencies": { - "@smithy/config-resolver": "^4.4.3", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "version": "4.2.32", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", + "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", + "dependencies": { + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3353,12 +3398,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", - "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", + "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3377,11 +3422,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", - "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", + "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3389,12 +3434,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", - "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", + "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "dependencies": { - "@smithy/service-error-classification": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3402,13 +3447,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", - "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "version": "4.5.11", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", + "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/types": "^4.9.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.9", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -4513,32 +4558,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cacache/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -4836,6 +4855,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, "engines": { "node": ">=10" } @@ -6851,9 +6871,9 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", + "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", "funding": [ { "type": "github", @@ -7268,6 +7288,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -9910,9 +9931,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -10333,6 +10354,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10409,6 +10431,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -11024,27 +11047,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/node-gyp/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-gyp/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-gyp/node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -11060,23 +11062,6 @@ "node": ">=6" } }, - "node_modules/node-gyp/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-oauth1": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/node-oauth1/-/node-oauth1-1.3.0.tgz", @@ -14255,41 +14240,6 @@ } } }, - "node_modules/sqlite3/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sqlite3/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sqlite3/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -15133,9 +15083,9 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", @@ -15412,9 +15362,9 @@ } }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -16481,7 +16431,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 5abc4eee..82dc52de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datasance/iofogcontroller", - "version": "3.5.12", + "version": "3.6.0", "description": "ioFog Controller project for Datasance PoT @ datasance.com \\nCopyright (c) 2023 Datasance Teknoloji A.S.", "main": "./src/main.js", "author": "Emirhan Durmus", @@ -55,7 +55,7 @@ "iofog-controller": "src/main.js" }, "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.948.0", + "@aws-sdk/client-secrets-manager": "^3.981.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/identity": "^4.13.0", "@datasance/ecn-viewer": "1.2.6", @@ -149,6 +149,8 @@ }, "sequelize": { "validator": "^13.15.22" - } + }, + "tar": "^7.5.7", + "lodash": "^4.17.23" } } diff --git a/src/config/controller.yaml b/src/config/config.yaml similarity index 95% rename from src/config/controller.yaml rename to src/config/config.yaml index d83653d0..558464dd 100644 --- a/src/config/controller.yaml +++ b/src/config/config.yaml @@ -1,6 +1,7 @@ # Application Configuration app: - name: pot-controller # Application name + name: pot # Application name + uuid: "" controlPlane: Remote # Control plane type: Remote or Kubernetes or Local namespace: datasance # Namespace for the application @@ -56,6 +57,9 @@ settings: eventCleanupInterval: 86400 # Cleanup job interval in seconds (default: 24 hours) eventAuditEnabled: true # Enable/disable event auditing eventCaptureIpAddress: true # Capture IP address (default: true, set false for privacy compliance) + controllerHeartbeatInterval: 30 # Controller heartbeat interval in seconds (default: 30) + controllerInactiveThreshold: 300 # Mark controller inactive after N seconds without heartbeat (default: 300 = 5 minutes) + controllerCleanupInterval: 600 # Controller cleanup job interval in seconds (default: 600 = 10 minutes) # Database Configuration database: diff --git a/src/config/env-mapping.js b/src/config/env-mapping.js index e1b5f4e6..9f4e255a 100644 --- a/src/config/env-mapping.js +++ b/src/config/env-mapping.js @@ -1,6 +1,7 @@ module.exports = { // Application Configuration - 'APP_NAME': 'app.name', + 'CONTROLLER_NAME': 'app.name', + 'CONTROLLER_UUID': 'app.uuid', 'CONTROL_PLANE': 'app.controlPlane', 'CONTROLLER_NAMESPACE': 'app.namespace', @@ -44,6 +45,9 @@ module.exports = { 'EVENT_CLEANUP_INTERVAL': 'settings.eventCleanupInterval', 'EVENT_AUDIT_ENABLED': 'settings.eventAuditEnabled', 'EVENT_CAPTURE_IP_ADDRESS': 'settings.eventCaptureIpAddress', + 'CONTROLLER_HEARTBEAT_INTERVAL': 'settings.controllerHeartbeatInterval', + 'CONTROLLER_INACTIVE_THRESHOLD': 'settings.controllerInactiveThreshold', + 'CONTROLLER_CLEANUP_INTERVAL': 'settings.controllerCleanupInterval', // Database Configuration 'DB_PROVIDER': 'database.provider', diff --git a/src/config/index.js b/src/config/index.js index c6e7da05..267697a4 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -19,7 +19,7 @@ const yaml = require('js-yaml') class Config { constructor () { this.envMapping = require('./env-mapping') - this.configPath = process.env.CONFIG_PATH || path.join(__dirname, 'controller.yaml') + this.configPath = process.env.CONFIG_PATH || path.join(__dirname, 'config.yaml') this.config = null this.load() } diff --git a/src/config/rbac-resources.yaml b/src/config/rbac-resources.yaml new file mode 100644 index 00000000..84b73593 --- /dev/null +++ b/src/config/rbac-resources.yaml @@ -0,0 +1,759 @@ +# RBAC Route Resource Catalog +# Maps API routes to RBAC resources and verbs +# Used by RBAC middleware to determine required permissions +# Note: Controller manages single namespace, namespace field is for K8s-style compatibility only + +resources: + # Microservices + microservices: + basePath: /api/v3/microservices + routes: + - path: /api/v3/microservices/ + methods: + GET: [list] + - path: /api/v3/microservices + methods: + POST: [create] + - path: /api/v3/microservices/yaml + methods: + POST: [create] + - path: /api/v3/microservices/:uuid + methods: + GET: [get] + PATCH: [patch] + PUT: [update] + DELETE: [delete] + resourceNameParam: uuid + - path: /api/v3/microservices/pub/:tag + methods: + GET: [get] + resourceNameParam: tag + - path: /api/v3/microservices/sub/:tag + methods: + GET: [get] + resourceNameParam: tag + - path: /api/v3/microservices/:uuid/rebuild + methods: + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/yaml/:uuid + methods: + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/config + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/routes/:receiverUuid + methods: + POST: [patch] + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/port-mapping + methods: + GET: [get] + POST: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/port-mapping/:internalPort + methods: + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/volume-mapping + methods: + GET: [get] + POST: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/volume-mapping/:id + methods: + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/start + methods: + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/stop + methods: + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/image-snapshot + methods: + GET: [get] + POST: [create] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/strace + methods: + GET: [get] + PATCH: [patch] + PUT: [update] + resourceNameParam: uuid + + # System Microservices + systemMicroservices: + routes: + - path: /api/v3/microservices/system + methods: + GET: [list] + - path: /api/v3/microservices/system/:uuid + methods: + GET: [get] + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/rebuild + methods: + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/yaml/:uuid + methods: + PATCH: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/config + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/port-mapping + methods: + POST: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/port-mapping/:internalPort + methods: + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/volume-mapping + methods: + POST: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/volume-mapping/:id + methods: + DELETE: [patch] + resourceNameParam: uuid + + # Fogs (ioFog agents) + fogs: + basePath: /api/v3/iofog + routes: + - path: /api/v3/iofog-list + methods: + GET: [list] + - path: /api/v3/iofog + methods: + POST: [create] + - path: /api/v3/iofog/:uuid + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: uuid + - path: /api/v3/iofog/:uuid/provisioning-key + methods: + GET: [get] + resourceNameParam: uuid + - path: /api/v3/iofog/:uuid/version/:versionCommand + methods: + POST: [patch] + resourceNameParam: uuid + - path: /api/v3/iofog/:uuid/reboot + methods: + POST: [patch] + resourceNameParam: uuid + - path: /api/v3/iofog/:uuid/hal/hw + methods: + GET: [get] + resourceNameParam: uuid + - path: /api/v3/iofog/:uuid/hal/usb + methods: + GET: [get] + resourceNameParam: uuid + - path: /api/v3/iofog/:uuid/prune + methods: + POST: [patch] + resourceNameParam: uuid + + # Exec Sessions + execSessions: + routes: + - path: /api/v3/microservices/:uuid/exec + methods: + POST: [patch] + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/exec/:microserviceUuid + methods: + WS: [get] + resourceNameParam: microserviceUuid + + # System Exec Sessions + systemExecSessions: + routes: + - path: /api/v3/iofog/:uuid/exec + methods: + POST: [patch] + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/exec + methods: + POST: [patch] + DELETE: [patch] + resourceNameParam: uuid + - path: /api/v3/microservices/system/exec/:microserviceUuid + methods: + WS: [get] + resourceNameParam: microserviceUuid + # Logs + logs: + routes: + - path: /api/v3/microservices/:uuid/logs + methods: + WS: [get] + resourceNameParam: uuid + + # System Logs + systemLogs: + routes: + - path: /api/v3/iofog/:uuid/logs + methods: + WS: [get] + resourceNameParam: uuid + - path: /api/v3/microservices/system/:uuid/logs + methods: + WS: [get] + resourceNameParam: uuid + + # Applications + applications: + basePath: /api/v3/application + routes: + - path: /api/v3/application + methods: + GET: [list] + POST: [create] + - path: /api/v3/application/yaml + methods: + POST: [create] + - path: /api/v3/application/:name + methods: + GET: [get] + PATCH: [patch] + PUT: [update] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/application/yaml/:name + methods: + PUT: [update] + + # System Applications + systemApplications: + routes: + - path: /api/v3/application/system + methods: + GET: [list] + - path: /api/v3/application/system/:name + methods: + GET: [get] + DELETE: [delete] + resourceNameParam: name + + # Cluster Controllers + cluster: + basePath: /api/v3/cluster + routes: + - path: /api/v3/cluster/controllers + methods: + GET: [list] + - path: /api/v3/cluster/controllers/:uuid + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: uuid + + # Routings + routings: + basePath: /api/v3/routes + routes: + - path: /api/v3/routes + methods: + GET: [list] + POST: [create] + - path: /api/v3/routes/:appName/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + + # Router + router: + basePath: /api/v3/router + routes: + - path: /api/v3/router + methods: + GET: [get] + PUT: [update] + + # Services + services: + basePath: /api/v3/services + routes: + - path: /api/v3/services + methods: + GET: [list] + POST: [create] + - path: /api/v3/services/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/services/yaml + methods: + POST: [create] + - path: /api/v3/services/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + + # Flows + flows: + basePath: /api/v3/flow + routes: + - path: /api/v3/flow + methods: + GET: [list] + POST: [create] + - path: /api/v3/flow/:id + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: id + + # Catalog + catalog: + basePath: /api/v3/catalog + routes: + - path: /api/v3/catalog/microservices + methods: + GET: [list] + POST: [create] + - path: /api/v3/catalog/microservices/:id + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: id + - path: /api/v3/catalog/microservices/:id/images + methods: + GET: [get] + POST: [create] + DELETE: [delete] + resourceNameParam: id + + # Registries + registries: + basePath: /api/v3/registries + routes: + - path: /api/v3/registries + methods: + GET: [list] + POST: [create] + - path: /api/v3/registries/:id + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: id + + # Secrets + secrets: + basePath: /api/v3/secrets + routes: + - path: /api/v3/secrets + methods: + GET: [list] + POST: [create] + - path: /api/v3/secrets/yaml + methods: + POST: [create] + - path: /api/v3/secrets/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/secrets/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + + # ConfigMaps + configMaps: + basePath: /api/v3/configmaps + routes: + - path: /api/v3/configmaps + methods: + GET: [list] + POST: [create] + - path: /api/v3/configmaps/yaml + methods: + POST: [create] + - path: /api/v3/configmaps/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/configmaps/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + + # Volume Mounts + volumeMounts: + basePath: /api/v3/volumeMounts + routes: + - path: /api/v3/volumeMounts + methods: + GET: [list] + POST: [create] + - path: /api/v3/volumeMounts/yaml + methods: + POST: [create] + - path: /api/v3/volumeMounts/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/volumeMounts/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + - path: /api/v3/volumeMounts/:name/link + methods: + GET: [get] + POST: [patch] + DELETE: [patch] + resourceNameParam: name + + # Tunnels + tunnels: + basePath: /api/v3/iofog + routes: + - path: /api/v3/iofog/:id/tunnel + methods: + GET: [get] + PATCH: [patch] + resourceNameParam: id + + # Certificates + certificates: + basePath: /api/v3/certificates + routes: + - path: /api/v3/certificates/ca + methods: + GET: [list] + POST: [create] + - path: /api/v3/certificates/ca/:name + methods: + GET: [get] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/certificates + methods: + GET: [list] + POST: [create] + - path: /api/v3/certificates/expiring + methods: + GET: [list] + - path: /api/v3/certificates/:name + methods: + GET: [get] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/certificates/:name/renew + methods: + POST: [patch] + resourceNameParam: name + - path: /api/v3/certificates/yaml + methods: + POST: [create] + + # Edge Resources + edgeResources: + basePath: /api/v3/edgeResource + routes: + - path: /api/v3/edgeResources + methods: + GET: [list] + - path: /api/v3/edgeResource/:name/:version + methods: + GET: [get] + PUT: [update] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/edgeResource/:name + methods: + GET: [get] + resourceNameParam: name + - path: /api/v3/edgeResource + methods: + POST: [create] + - path: /api/v3/edgeResource/:name/:version/link + methods: + POST: [patch] + DELETE: [patch] + resourceNameParam: name + + # Application Templates + applicationTemplates: + basePath: /api/v3/applicationTemplate + routes: + - path: /api/v3/applicationTemplates + methods: + GET: [list] + - path: /api/v3/applicationTemplate + methods: + POST: [create] + - path: /api/v3/applicationTemplate/yaml + methods: + POST: [create] + - path: /api/v3/applicationTemplate/:name + methods: + GET: [get] + PATCH: [patch] + PUT: [update] + DELETE: [delete] + resourceNameParam: name + - path: /api/v3/applicationTemplate/yaml/:name + methods: + PUT: [update] + resourceNameParam: name + + # Capabilities + capabilities: + basePath: /api/v3/capabilities + routes: + - path: /api/v3/capabilities/edgeResources + methods: + HEAD: [get] + - path: /api/v3/capabilities/applicationTemplates + methods: + HEAD: [get] + + # Events + events: + basePath: /api/v3/events + routes: + - path: /api/v3/events + methods: + GET: [list] + DELETE: [delete] + + # Diagnostics + diagnostics: + basePath: /api/v3/microservices + routes: + - path: /api/v3/microservices/:uuid/image-snapshot + methods: + GET: [get] + POST: [create] + resourceNameParam: uuid + - path: /api/v3/microservices/:uuid/strace + methods: + GET: [get] + PATCH: [patch] + PUT: [update] + resourceNameParam: uuid + + # Agent endpoints (for agent-to-controller communication) + agent: + basePath: /api/v3/agent + routes: + - path: /api/v3/agent/provision + methods: + POST: [create] + - path: /api/v3/agent/deprovision + methods: + POST: [delete] + - path: /api/v3/agent/config + methods: + GET: [get] + PATCH: [patch] + - path: /api/v3/agent/config/gps + methods: + PATCH: [patch] + - path: /api/v3/agent/config/changes + methods: + GET: [get] + PATCH: [patch] + - path: /api/v3/agent/status + methods: + PUT: [update] + - path: /api/v3/agent/edgeResources + methods: + GET: [list] + - path: /api/v3/agent/volumeMounts + methods: + GET: [list] + - path: /api/v3/agent/microservices + methods: + GET: [list] + - path: /api/v3/agent/microservices/:microserviceUuid + methods: + GET: [get] + resourceNameParam: microserviceUuid + - path: /api/v3/agent/registries + methods: + GET: [list] + - path: /api/v3/agent/tunnel + methods: + GET: [get] + - path: /api/v3/agent/strace + methods: + GET: [get] + PUT: [update] + - path: /api/v3/agent/version + methods: + GET: [get] + - path: /api/v3/agent/hal/hw + methods: + PUT: [update] + - path: /api/v3/agent/hal/usb + methods: + PUT: [update] + - path: /api/v3/agent/delete-node + methods: + DELETE: [delete] + - path: /api/v3/agent/image-snapshot + methods: + GET: [get] + PUT: [update] + - path: /api/v3/agent/cert + methods: + GET: [get] + - path: /api/v3/agent/logs/sessions + methods: + GET: [list] + - path: /api/v3/agent/exec/:microserviceUuid + methods: + WS: [get] + resourceNameParam: microserviceUuid + - path: /api/v3/agent/logs/microservice/:microserviceUuid/:sessionId + methods: + WS: [get] + resourceNameParam: microserviceUuid + - path: /api/v3/agent/logs/iofog/:iofogUuid/:sessionId + methods: + WS: [get] + resourceNameParam: iofogUuid + + # User endpoints + users: + basePath: /api/v3/user + routes: + - path: /api/v3/user/login + methods: + POST: [] + - path: /api/v3/user/refresh + methods: + POST: [] + - path: /api/v3/user/profile + methods: + GET: [get] + - path: /api/v3/user/logout + methods: + POST: [] + + # Config management + config: + basePath: /api/v3/config + routes: + - path: /api/v3/config + methods: + GET: [list] + PUT: [update] + - path: /api/v3/config/:key + methods: + GET: [get] + resourceNameParam: key + + # Controller status (public, no auth required) + controller: + basePath: /api/v3 + routes: + - path: /api/v3/status + methods: + GET: [] + - path: /api/v3/fog-types/ + methods: + GET: [] + + # RBAC management endpoints + roles: + basePath: /api/v3/roles + routes: + - path: /api/v3/roles + methods: + GET: [list] + POST: [create] + - path: /api/v3/roles/yaml + methods: + POST: [create] + - path: /api/v3/roles/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + - path: /api/v3/roles/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + + roleBindings: + basePath: /api/v3/rolebindings + routes: + - path: /api/v3/rolebindings + methods: + GET: [list] + POST: [create] + - path: /api/v3/rolebindings/yaml + methods: + POST: [create] + - path: /api/v3/rolebindings/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + - path: /api/v3/rolebindings/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + + serviceAccounts: + basePath: /api/v3/serviceaccounts + routes: + - path: /api/v3/serviceaccounts + methods: + GET: [list] + POST: [create] + - path: /api/v3/serviceaccounts/yaml + methods: + POST: [create] + - path: /api/v3/serviceaccounts/yaml/:name + methods: + PATCH: [patch] + resourceNameParam: name + - path: /api/v3/serviceaccounts/:name + methods: + GET: [get] + PATCH: [patch] + DELETE: [delete] + resourceNameParam: name + + diff --git a/src/config/rbac-system-roles.js b/src/config/rbac-system-roles.js new file mode 100644 index 00000000..f15237cc --- /dev/null +++ b/src/config/rbac-system-roles.js @@ -0,0 +1,157 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +/** + * Hardcoded system roles configuration + * Admin role is fixed and cannot be modified, created, or deleted + * Note: Namespace is set from controller config at runtime, 'datasance' is default + */ +const config = require('./index') + +function getNamespace () { + return process.env.CONTROLLER_NAMESPACE || config.get('app.namespace', 'datasance') +} + +module.exports = { + ADMIN_ROLE: { + name: 'admin', + apiVersion: 'datasance.com/v3', + kind: 'Role', + get namespace () { + return getNamespace() + }, + rules: [ + { + apiGroups: [''], + resources: ['*'], + verbs: ['*'] + } + ] + }, + SRE_ROLE: { + name: 'sre', + apiVersion: 'datasance.com/v3', + kind: 'Role', + get namespace () { + return getNamespace() + }, + rules: [ + { + apiGroups: [''], + resources: ['microservices', 'systemMicroservices', 'fogs', 'applications', 'systemApplications', 'applicationTemplates', 'services', 'routings', 'router', 'flows', 'catalog', 'registries', 'secrets', 'configMaps', 'volumeMounts', 'tunnels', 'certificates', 'edgeResources', 'capabilities', 'diagnostics', 'serviceAccounts', 'events', 'users', 'config', 'controller', 'execSessions', 'systemExecSessions', 'logs', 'systemLogs'], + verbs: ['*'] + }, + { + apiGroups: [''], + resources: ['roles', 'roleBindings'], + verbs: ['get', 'list'] + } + ] + }, + DEVELOPER_ROLE: { + name: 'developer', + apiVersion: 'datasance.com/v3', + kind: 'Role', + get namespace () { + return getNamespace() + }, + rules: [ + { + apiGroups: [''], + resources: ['microservices', 'applications', 'applicationTemplates', 'services', 'routings', 'flows', 'catalog', 'registries', 'secrets', 'configMaps', 'volumeMounts', 'certificates', 'edgeResources', 'capabilities', 'diagnostics', 'serviceAccounts', 'controller', 'execSessions', 'logs'], + verbs: ['get', 'list', 'create', 'update', 'patch', 'delete'] + }, + { + apiGroups: [''], + resources: ['fogs', 'router', 'tunnels', 'users', 'config', 'roles', 'roleBindings', 'systemMicroservices', 'systemApplications'], + verbs: ['get', 'list'] + } + ] + }, + VIEWER_ROLE: { + name: 'viewer', + apiVersion: 'datasance.com/v3', + kind: 'Role', + get namespace () { + return getNamespace() + }, + rules: [ + { + apiGroups: [''], + resources: ['microservices', 'fogs', 'applications', 'systemMicroservices', 'systemApplications', 'applicationTemplates', 'services', 'routings', 'router', 'flows', 'catalog', 'registries', 'secrets', 'configMaps', 'volumeMounts', 'certificates', 'edgeResources', 'capabilities', 'diagnostics', 'serviceAccounts', 'config', 'controller', 'roles', 'roleBindings'], + verbs: ['get', 'list'] + } + ] + }, + AGENT_ADMIN_ROLE: { + name: 'agent-admin', + apiVersion: 'agent.datasance.com/v3', + kind: 'Role', + get namespace () { + return getNamespace() + }, + rules: [ + { + // Wildcard covers all agent API resources and verbs + // This includes all Microservice role permissions plus: + // - status (get) + // - info (get) + // - version (get) + // - provision (post) + // - deprovision (delete) + // - config (post) + // - prune (post) + apiGroups: ['agent.datasance.com/v3'], + resources: ['*'], + verbs: ['*'] + } + ] + }, + MICROSERVICE_ROLE: { + name: 'microservice', + apiVersion: 'agent.datasance.com/v3', + kind: 'Role', + get namespace () { + return getNamespace() + }, + rules: [ + { + apiGroups: ['agent.datasance.com/v3'], + resources: ['gps'], + verbs: ['get', 'patch'] + }, + { + apiGroups: ['agent.datasance.com/v3'], + resources: ['config'], + verbs: ['get'] + }, + { + apiGroups: ['agent.datasance.com/v3'], + resources: ['message'], + verbs: ['post'] + // Note: WebSocket 'get' for message is handled separately by agent + }, + { + apiGroups: ['agent.datasance.com/v3'], + resources: ['log'], + verbs: ['post'] + }, + { + apiGroups: ['agent.datasance.com/v3'], + resources: ['control'], + verbs: ['get'] + // Note: WebSocket 'get' for control is handled separately by agent + } + ] + } +} diff --git a/src/controllers/cluster-controller.js b/src/controllers/cluster-controller.js new file mode 100644 index 00000000..e937d11b --- /dev/null +++ b/src/controllers/cluster-controller.js @@ -0,0 +1,41 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const ClusterControllerService = require('../services/cluster-controller-service') + +const listClusterControllersEndPoint = async function (req) { + return ClusterControllerService.listClusterControllers(false) +} + +const getClusterControllerEndPoint = async function (req) { + const uuid = req.params.uuid + return ClusterControllerService.getClusterController(uuid, false) +} + +const updateClusterControllerEndPoint = async function (req) { + const uuid = req.params.uuid + const data = req.body + return ClusterControllerService.updateClusterController(uuid, data, false) +} + +const deleteClusterControllerEndPoint = async function (req) { + const uuid = req.params.uuid + return ClusterControllerService.deleteClusterController(uuid, false) +} + +module.exports = { + listClusterControllersEndPoint, + getClusterControllerEndPoint, + updateClusterControllerEndPoint, + deleteClusterControllerEndPoint +} diff --git a/src/controllers/rbac-controller.js b/src/controllers/rbac-controller.js new file mode 100644 index 00000000..4760f55f --- /dev/null +++ b/src/controllers/rbac-controller.js @@ -0,0 +1,160 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const RbacService = require('../services/rbac-service') +const YamlParserService = require('../services/yaml-parser-service') + +// Role Endpoints +const listRolesEndpoint = async function (req) { + return RbacService.listRolesEndpoint() +} + +const getRoleEndpoint = async function (req) { + const name = req.params.name + return RbacService.getRoleEndpoint(name) +} + +const createRoleEndpoint = async function (req) { + const roleData = req.body + return RbacService.createRoleEndpoint(roleData) +} + +const updateRoleEndpoint = async function (req) { + const name = req.params.name + const roleData = req.body + return RbacService.updateRoleEndpoint(name, roleData) +} + +const deleteRoleEndpoint = async function (req) { + const name = req.params.name + return RbacService.deleteRoleEndpoint(name) +} + +// RoleBinding Endpoints +const listRoleBindingsEndpoint = async function (req) { + return RbacService.listRoleBindingsEndpoint() +} + +const getRoleBindingEndpoint = async function (req) { + const name = req.params.name + return RbacService.getRoleBindingEndpoint(name) +} + +const createRoleBindingEndpoint = async function (req) { + const bindingData = req.body + return RbacService.createRoleBindingEndpoint(bindingData) +} + +const updateRoleBindingEndpoint = async function (req) { + const name = req.params.name + const bindingData = req.body + return RbacService.updateRoleBindingEndpoint(name, bindingData) +} + +const deleteRoleBindingEndpoint = async function (req) { + const name = req.params.name + return RbacService.deleteRoleBindingEndpoint(name) +} + +// ServiceAccount Endpoints +const listServiceAccountsEndpoint = async function (req) { + return RbacService.listServiceAccountsEndpoint() +} + +const getServiceAccountEndpoint = async function (req) { + const name = req.params.name + return RbacService.getServiceAccountEndpoint(name) +} + +const createServiceAccountEndpoint = async function (req) { + const saData = req.body + return RbacService.createServiceAccountEndpoint(saData) +} + +const updateServiceAccountEndpoint = async function (req) { + const name = req.params.name + const saData = req.body + return RbacService.updateServiceAccountEndpoint(name, saData) +} + +const deleteServiceAccountEndpoint = async function (req) { + const name = req.params.name + return RbacService.deleteServiceAccountEndpoint(name) +} + +// YAML Endpoints +const createRoleFromYamlEndpoint = async function (req) { + const fileContent = req.file.buffer.toString() + const roleData = await YamlParserService.parseRoleFile(fileContent) + return RbacService.createRoleEndpoint(roleData) +} + +const updateRoleFromYamlEndpoint = async function (req) { + const name = req.params.name + const fileContent = req.file.buffer.toString() + const roleData = await YamlParserService.parseRoleFile(fileContent) + return RbacService.updateRoleEndpoint(name, roleData) +} + +const createRoleBindingFromYamlEndpoint = async function (req) { + const fileContent = req.file.buffer.toString() + const bindingData = await YamlParserService.parseRoleBindingFile(fileContent) + return RbacService.createRoleBindingEndpoint(bindingData) +} + +const updateRoleBindingFromYamlEndpoint = async function (req) { + const name = req.params.name + const fileContent = req.file.buffer.toString() + const bindingData = await YamlParserService.parseRoleBindingFile(fileContent) + return RbacService.updateRoleBindingEndpoint(name, bindingData) +} + +const createServiceAccountFromYamlEndpoint = async function (req) { + const fileContent = req.file.buffer.toString() + const saData = await YamlParserService.parseServiceAccountFile(fileContent) + return RbacService.createServiceAccountEndpoint(saData) +} + +const updateServiceAccountFromYamlEndpoint = async function (req) { + const name = req.params.name + const fileContent = req.file.buffer.toString() + const saData = await YamlParserService.parseServiceAccountFile(fileContent) + return RbacService.updateServiceAccountEndpoint(name, saData) +} + +module.exports = { + // Role endpoints + listRolesEndpoint: (listRolesEndpoint), + getRoleEndpoint: (getRoleEndpoint), + createRoleEndpoint: (createRoleEndpoint), + updateRoleEndpoint: (updateRoleEndpoint), + deleteRoleEndpoint: (deleteRoleEndpoint), + createRoleFromYamlEndpoint: (createRoleFromYamlEndpoint), + updateRoleFromYamlEndpoint: (updateRoleFromYamlEndpoint), + // RoleBinding endpoints + listRoleBindingsEndpoint: (listRoleBindingsEndpoint), + getRoleBindingEndpoint: (getRoleBindingEndpoint), + createRoleBindingEndpoint: (createRoleBindingEndpoint), + updateRoleBindingEndpoint: (updateRoleBindingEndpoint), + deleteRoleBindingEndpoint: (deleteRoleBindingEndpoint), + createRoleBindingFromYamlEndpoint: (createRoleBindingFromYamlEndpoint), + updateRoleBindingFromYamlEndpoint: (updateRoleBindingFromYamlEndpoint), + // ServiceAccount endpoints + listServiceAccountsEndpoint: (listServiceAccountsEndpoint), + getServiceAccountEndpoint: (getServiceAccountEndpoint), + createServiceAccountEndpoint: (createServiceAccountEndpoint), + updateServiceAccountEndpoint: (updateServiceAccountEndpoint), + deleteServiceAccountEndpoint: (deleteServiceAccountEndpoint), + createServiceAccountFromYamlEndpoint: (createServiceAccountFromYamlEndpoint), + updateServiceAccountFromYamlEndpoint: (updateServiceAccountFromYamlEndpoint) +} diff --git a/src/controllers/registry-controller.js b/src/controllers/registry-controller.js index ab633cb0..84b240e6 100644 --- a/src/controllers/registry-controller.js +++ b/src/controllers/registry-controller.js @@ -22,6 +22,10 @@ const getRegistriesEndPoint = async function (req) { return RegistryService.findRegistries(false) } +const getRegistryEndPoint = async function (req) { + const registryId = req.params.id + return RegistryService.getRegistry(registryId, false) +} const deleteRegistryEndPoint = async function (req) { const deleteRegistry = { id: parseInt(req.params.id) @@ -38,6 +42,7 @@ const updateRegistryEndPoint = async function (req) { module.exports = { createRegistryEndPoint: (createRegistryEndPoint), getRegistriesEndPoint: (getRegistriesEndPoint), + getRegistryEndPoint: (getRegistryEndPoint), deleteRegistryEndPoint: (deleteRegistryEndPoint), updateRegistryEndPoint: (updateRegistryEndPoint) } diff --git a/src/data/managers/cluster-controller-manager.js b/src/data/managers/cluster-controller-manager.js new file mode 100644 index 00000000..7b7d767c --- /dev/null +++ b/src/data/managers/cluster-controller-manager.js @@ -0,0 +1,23 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const ClusterController = require('../models').ClusterController + +class ClusterControllerManager extends BaseManager { + getEntity () { + return ClusterController + } +} + +module.exports = new ClusterControllerManager() diff --git a/src/data/managers/microservice-manager.js b/src/data/managers/microservice-manager.js index 6c50fcd0..a5d62abf 100644 --- a/src/data/managers/microservice-manager.js +++ b/src/data/managers/microservice-manager.js @@ -33,6 +33,7 @@ const Registry = models.Registry const MicroserviceStatus = models.MicroserviceStatus const MicroserviceExecStatus = models.MicroserviceExecStatus const MicroserviceHealthCheck = models.MicroserviceHealthCheck +const RbacServiceAccount = models.RbacServiceAccount const Op = require('sequelize').Op const microserviceExcludedFields = [ @@ -154,6 +155,11 @@ class MicroserviceManager extends BaseManager { as: 'healthCheck', required: false, attributes: ['test', 'interval', 'timeout', 'startPeriod', 'startInterval', 'retries'] + }, + { + model: RbacServiceAccount, + as: 'serviceAccount', + required: false } ], where: where, @@ -266,6 +272,11 @@ class MicroserviceManager extends BaseManager { as: 'healthCheck', required: false, attributes: ['test', 'interval', 'timeout', 'startPeriod', 'startInterval', 'retries'] + }, + { + model: RbacServiceAccount, + as: 'serviceAccount', + required: false } ], where: { @@ -390,6 +401,11 @@ class MicroserviceManager extends BaseManager { as: 'healthCheck', required: false, attributes: ['test', 'interval', 'timeout', 'startPeriod', 'startInterval', 'retries'] + }, + { + model: RbacServiceAccount, + as: 'serviceAccount', + required: false } ], where: where, @@ -481,6 +497,11 @@ class MicroserviceManager extends BaseManager { as: 'subTags', attributes: ['value'], through: { attributes: [] } + }, + { + model: RbacServiceAccount, + as: 'serviceAccount', + required: false } ], where: where, @@ -510,6 +531,11 @@ class MicroserviceManager extends BaseManager { as: 'subTags', attributes: ['value'], through: { attributes: [] } + }, + { + model: RbacServiceAccount, + as: 'serviceAccount', + required: false } ], where: where, @@ -540,6 +566,11 @@ class MicroserviceManager extends BaseManager { as: 'subTags', attributes: ['value'], through: { attributes: [] } + }, + { + model: RbacServiceAccount, + as: 'serviceAccount', + required: false } ], where: where, diff --git a/src/data/managers/rbac-cache-version-manager.js b/src/data/managers/rbac-cache-version-manager.js new file mode 100644 index 00000000..73666a65 --- /dev/null +++ b/src/data/managers/rbac-cache-version-manager.js @@ -0,0 +1,70 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const models = require('../models') +const RbacCacheVersion = models.RbacCacheVersion + +class RbacCacheVersionManager extends BaseManager { + getEntity () { + return RbacCacheVersion + } + + /** + * Get current cache version + * @param {Object} transaction - Database transaction + * @returns {Promise} Current version number + */ + async getVersion (transaction) { + const cacheVersion = await this.findOne({ id: 1 }, transaction) + if (!cacheVersion) { + return 0 + } + return cacheVersion.version || 0 + } + + /** + * Increment cache version + * This should be called whenever any RBAC resource (Role, RoleBinding, ServiceAccount) is modified + * @param {Object} transaction - Database transaction + * @returns {Promise} + */ + async incrementVersion (transaction) { + const cacheVersion = await this.findOne({ id: 1 }, transaction) + + if (cacheVersion) { + // Update existing version + const newVersion = (cacheVersion.version || 0) + 1 + await this.update({ id: 1 }, { version: newVersion }, transaction) + } else { + // Create initial version if it doesn't exist + await this.create({ id: 1, version: 1 }, transaction) + } + } + + /** + * Initialize cache version row if it doesn't exist + * This is called on server startup to ensure the row exists + * @param {Object} transaction - Database transaction (optional) + * @returns {Promise} + */ + async initializeVersion (transaction) { + const cacheVersion = await this.findOne({ id: 1 }, transaction) + if (!cacheVersion) { + // Create initial version row + await this.create({ id: 1, version: 1 }, transaction) + } + } +} + +module.exports = new RbacCacheVersionManager() diff --git a/src/data/managers/rbac-role-binding-manager.js b/src/data/managers/rbac-role-binding-manager.js new file mode 100644 index 00000000..a04c1c49 --- /dev/null +++ b/src/data/managers/rbac-role-binding-manager.js @@ -0,0 +1,181 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const models = require('../models') +const RbacRoleBinding = models.RbacRoleBinding +const RbacRoleManager = require('./rbac-role-manager') +const Errors = require('../../helpers/errors') +const RbacCacheVersionManager = require('./rbac-cache-version-manager') + +class RbacRoleBindingManager extends BaseManager { + getEntity () { + return RbacRoleBinding + } + + /** + * Create a RoleBinding + */ + async createRoleBinding (bindingData, transaction) { + // Check if RoleBinding already exists + const existingBinding = await this.findOne({ name: bindingData.name }, transaction) + if (existingBinding) { + throw new Errors.ConflictError(`RoleBinding '${bindingData.name}' already exists`) + } + + // Validate role reference exists and get role ID + let roleId = null + if (bindingData.roleRef && bindingData.roleRef.name) { + // Use findOne to get the actual model with id + const role = await RbacRoleManager.findOne({ name: bindingData.roleRef.name }, transaction) + if (!role) { + // Check if it's a system role + const roleWithRules = await RbacRoleManager.getRoleWithRules(bindingData.roleRef.name, transaction) + if (!roleWithRules) { + throw new Errors.ValidationError(`Referenced role '${bindingData.roleRef.name}' does not exist`) + } + // System role - roleId will be null (system roles don't have DB ids) + } else { + roleId = role.id + } + } + + let binding + try { + binding = await this.create({ + name: bindingData.name, + // apiVersion removed + kind: bindingData.kind || 'RoleBinding', + // namespace removed (controller manages single namespace) + roleRef: bindingData.roleRef, + roleId: roleId, + subjects: bindingData.subjects || [] + }, transaction) + } catch (error) { + // Handle SequelizeUniqueConstraintError as a safety net + if (error.name === 'SequelizeUniqueConstraintError') { + throw new Errors.ConflictError(`RoleBinding '${bindingData.name}' already exists`) + } + throw error + } + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return binding + } + + /** + * Update a RoleBinding + */ + async updateRoleBinding (name, bindingData, transaction) { + const binding = await this.findOne({ name }, transaction) + if (!binding) { + throw new Errors.NotFoundError(`RoleBinding '${name}' not found`) + } + + // Validate role reference if provided and get role ID + let roleId = null + if (bindingData.roleRef && bindingData.roleRef.name) { + // Use findOne to get the actual model with id + const role = await RbacRoleManager.findOne({ name: bindingData.roleRef.name }, transaction) + if (!role) { + // Check if it's a system role + const roleWithRules = await RbacRoleManager.getRoleWithRules(bindingData.roleRef.name, transaction) + if (!roleWithRules) { + throw new Errors.ValidationError(`Referenced role '${bindingData.roleRef.name}' does not exist`) + } + // System role - roleId will be null (system roles don't have DB ids) + } else { + roleId = role.id + } + } else if (bindingData.roleRef === undefined) { + // If roleRef is not provided, keep existing roleId + const existingRoleRef = binding.roleRef + if (existingRoleRef && existingRoleRef.name) { + const role = await RbacRoleManager.findOne({ name: existingRoleRef.name }, transaction) + if (role) { + roleId = role.id + } + // If not found in DB, it might be a system role - keep roleId as null + } + } + + const updateData = {} + if (bindingData.name) updateData.name = bindingData.name + // apiVersion and namespace removed (not stored in database) + if (bindingData.roleRef) updateData.roleRef = bindingData.roleRef + if (roleId !== null) updateData.roleId = roleId + if (bindingData.subjects) updateData.subjects = bindingData.subjects + + await this.update({ name }, updateData, transaction) + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return this.findOne({ name: bindingData.name || name }, transaction) + } + + /** + * Delete a RoleBinding + */ + async deleteRoleBinding (name, transaction) { + const binding = await this.findOne({ name }, transaction) + if (!binding) { + throw new Errors.NotFoundError(`RoleBinding '${name}' not found`) + } + + await this.delete({ name }, transaction) + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return { message: `RoleBinding '${name}' deleted successfully` } + } + + /** + * Get a RoleBinding by name + */ + async getRoleBinding (name, transaction) { + return this.findOne({ name }, transaction) + } + + /** + * List all RoleBindings + */ + async listRoleBindings (transaction) { + return this.findAll({}, transaction) + } + + /** + * Find RoleBindings by subject + */ + async findRoleBindingsBySubject (subject, transaction) { + const bindings = await this.findAll({}, transaction) + const matchingBindings = [] + + for (const binding of bindings) { + const subjects = binding.subjects || [] + for (const subj of subjects) { + if (subj.kind === subject.kind && subj.name === subject.name) { + matchingBindings.push(binding) + break + } + } + } + + return matchingBindings + } +} + +module.exports = new RbacRoleBindingManager() diff --git a/src/data/managers/rbac-role-manager.js b/src/data/managers/rbac-role-manager.js new file mode 100644 index 00000000..43ee4191 --- /dev/null +++ b/src/data/managers/rbac-role-manager.js @@ -0,0 +1,293 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const models = require('../models') +const RbacRole = models.RbacRole +const RbacRoleRule = models.RbacRoleRule +const systemRoles = require('../../config/rbac-system-roles') +const Errors = require('../../helpers/errors') +const RbacCacheVersionManager = require('./rbac-cache-version-manager') + +class RbacRoleManager extends BaseManager { + getEntity () { + return RbacRole + } + + /** + * Check if role name is Admin (case-insensitive) + */ + isAdminRole (name) { + return name && name.toLowerCase() === 'admin' + } + + /** + * Check if role name is a system role (case-insensitive) + * System roles: Admin, SRE, Developer, Viewer, Agent Admin, Microservice (fully static, defined in config) + */ + isSystemRole (name) { + if (!name) return false + const systemRoles = ['admin', 'sre', 'developer', 'viewer', 'agent-admin', 'microservice'] + return systemRoles.includes(name.toLowerCase()) + } + + /** + * Get system role definition from config by name + * @param {string} name - Role name + * @returns {Object|null} System role definition or null if not found + */ + getSystemRole (name) { + if (!name) return null + const normalizedName = name.toLowerCase() + switch (normalizedName) { + case 'admin': + return systemRoles.ADMIN_ROLE + case 'sre': + return systemRoles.SRE_ROLE + case 'developer': + return systemRoles.DEVELOPER_ROLE + case 'viewer': + return systemRoles.VIEWER_ROLE + case 'agent-admin': + return systemRoles.AGENT_ADMIN_ROLE + case 'microservice': + return systemRoles.MICROSERVICE_ROLE + default: + return null + } + } + + /** + * Check if role name is a protected built-in role (case-insensitive) + * Protected roles: Admin, SRE, Developer, Viewer + * @deprecated Use isSystemRole instead + */ + isProtectedRole (name) { + return this.isSystemRole(name) + } + + /** + * Create a Role with its rules + */ + async createRole (roleData, transaction) { + // Prevent creating system role names (Admin, SRE, Developer, Viewer) + if (this.isSystemRole(roleData.name)) { + throw new Errors.ValidationError(`Cannot create ${roleData.name} role. ${roleData.name} is a system role and cannot be created.`) + } + + // Check if role already exists + const existingRole = await this.findOne({ name: roleData.name }, transaction) + if (existingRole) { + throw new Errors.ConflictError(`Role '${roleData.name}' already exists`) + } + + let role + try { + role = await this.create({ + name: roleData.name, + // apiVersion removed + kind: roleData.kind || 'Role' + // namespace removed (controller manages single namespace) + }, transaction) + } catch (error) { + // Handle SequelizeUniqueConstraintError as a safety net + if (error.name === 'SequelizeUniqueConstraintError') { + throw new Errors.ConflictError(`Role '${roleData.name}' already exists`) + } + throw error + } + + // Create rules if provided + if (roleData.rules && Array.isArray(roleData.rules)) { + const rules = roleData.rules.map(rule => ({ + roleId: role.id, + apiGroups: rule.apiGroups || [''], + resources: rule.resources || [], + verbs: rule.verbs || [], + resourceNames: rule.resourceNames || null + })) + + const bulkCreateOptions = transaction.fakeTransaction ? {} : { transaction } + await RbacRoleRule.bulkCreate(rules, bulkCreateOptions) + } + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return this.getRoleWithRules(role.name, transaction) + } + + /** + * Update a Role and its rules + */ + async updateRole (name, roleData, transaction) { + // Prevent updating system roles (Admin, SRE, Developer, Viewer) + if (this.isSystemRole(name)) { + throw new Errors.ValidationError(`Cannot update ${name} role. ${name} is a system role and cannot be modified.`) + } + + // Prevent renaming to system role names + if (roleData.name && this.isSystemRole(roleData.name)) { + throw new Errors.ValidationError(`Cannot rename role to ${roleData.name}. ${roleData.name} is a system role.`) + } + + const role = await this.findOne({ name }, transaction) + if (!role) { + throw new Errors.NotFoundError(`Role '${name}' not found`) + } + + // Update role metadata + const updateData = {} + if (roleData.name) updateData.name = roleData.name + // apiVersion and namespace removed (not stored in database) + + if (Object.keys(updateData).length > 0) { + await this.update({ name }, updateData, transaction) + } + + // Update rules if provided + if (roleData.rules && Array.isArray(roleData.rules)) { + // Delete existing rules + const destroyOptions = transaction.fakeTransaction + ? { where: { roleId: role.id } } + : { where: { roleId: role.id }, transaction } + await RbacRoleRule.destroy(destroyOptions) + + // Create new rules + const rules = roleData.rules.map(rule => ({ + roleId: role.id, + apiGroups: rule.apiGroups || [''], + resources: rule.resources || [], + verbs: rule.verbs || [], + resourceNames: rule.resourceNames || null + })) + + const bulkCreateOptions = transaction.fakeTransaction ? {} : { transaction } + await RbacRoleRule.bulkCreate(rules, bulkCreateOptions) + } + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return this.getRoleWithRules(roleData.name || name, transaction) + } + + /** + * Delete a Role + */ + async deleteRole (name, transaction) { + // Prevent deleting system roles (Admin, SRE, Developer, Viewer) + if (this.isSystemRole(name)) { + throw new Errors.ValidationError(`Cannot delete ${name} role. ${name} is a system role and cannot be deleted.`) + } + + const role = await this.findOne({ name }, transaction) + if (!role) { + throw new Errors.NotFoundError(`Role '${name}' not found`) + } + + // Rules will be deleted via CASCADE + await this.delete({ name }, transaction) + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return { message: `Role '${name}' deleted successfully` } + } + + /** + * Get a Role with its rules + */ + async getRoleWithRules (name, transaction) { + // Check if it's a system role (Admin, SRE, Developer, Viewer) + if (this.isSystemRole(name)) { + const systemRole = this.getSystemRole(name) + if (systemRole) { + return { + name: systemRole.name, + // apiVersion removed - not stored in database (system roles config can keep it as metadata) + kind: systemRole.kind, + // namespace removed - not stored in database (controller manages single namespace) + rules: systemRole.rules + } + } + } + + const role = await this.findOne({ name }, transaction) + if (!role) { + return null + } + + const findAllOptions = transaction.fakeTransaction + ? { where: { roleId: role.id } } + : { where: { roleId: role.id }, transaction: transaction } + const rules = await RbacRoleRule.findAll(findAllOptions) + + return { + id: role.id, + name: role.name, + // apiVersion removed - not stored in database + kind: role.kind, + // namespace removed - not stored in database (controller manages single namespace) + rules: rules.map(rule => ({ + apiGroups: rule.apiGroups, + resources: rule.resources, + verbs: rule.verbs, + resourceNames: rule.resourceNames + })), + createdAt: role.createdAt, + updatedAt: role.updatedAt + } + } + + /** + * List all Roles with their rules + */ + async listRoles (transaction) { + const roles = await this.findAll({}, transaction) + const rolesWithRules = [] + + // Add database roles (excluding system roles that may exist from old migrations) + for (const role of roles) { + // Skip database entries for system roles (they're handled statically) + if (!this.isSystemRole(role.name)) { + const roleWithRules = await this.getRoleWithRules(role.name, transaction) + rolesWithRules.push(roleWithRules) + } + } + + // Add system roles to the list (Admin, SRE, Developer, Viewer, Agent Admin, Microservice) + const systemRolesList = [ + systemRoles.ADMIN_ROLE, + systemRoles.SRE_ROLE, + systemRoles.DEVELOPER_ROLE, + systemRoles.VIEWER_ROLE, + systemRoles.AGENT_ADMIN_ROLE, + systemRoles.MICROSERVICE_ROLE + ] + + for (const systemRole of systemRolesList) { + rolesWithRules.unshift({ + name: systemRole.name, + // apiVersion removed - not stored in database (system roles config can keep it as metadata) + kind: systemRole.kind, + // namespace removed - not stored in database (controller manages single namespace) + rules: systemRole.rules + }) + } + + return rolesWithRules + } +} + +module.exports = new RbacRoleManager() diff --git a/src/data/managers/rbac-service-account-manager.js b/src/data/managers/rbac-service-account-manager.js new file mode 100644 index 00000000..21280e30 --- /dev/null +++ b/src/data/managers/rbac-service-account-manager.js @@ -0,0 +1,162 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const BaseManager = require('./base-manager') +const models = require('../models') +const RbacServiceAccount = models.RbacServiceAccount +const RbacRoleManager = require('./rbac-role-manager') +const Errors = require('../../helpers/errors') +const RbacCacheVersionManager = require('./rbac-cache-version-manager') + +class RbacServiceAccountManager extends BaseManager { + getEntity () { + return RbacServiceAccount + } + + /** + * Create a ServiceAccount + * roleRef is required + */ + async createServiceAccount (saData, transaction) { + // Check if ServiceAccount already exists + const existingServiceAccount = await this.findOne({ name: saData.name }, transaction) + if (existingServiceAccount) { + throw new Errors.ConflictError(`ServiceAccount '${saData.name}' already exists`) + } + + // Validate roleRef is provided + if (!saData.roleRef || !saData.roleRef.name) { + throw new Errors.ValidationError('ServiceAccount must have a roleRef with a name.') + } + + // Validate role reference exists - use findOne to get the actual model with id + // System roles don't exist in DB, so we can't set roleId for them + const role = await RbacRoleManager.findOne({ name: saData.roleRef.name }, transaction) + if (!role) { + // Check if it's a system role + const roleWithRules = await RbacRoleManager.getRoleWithRules(saData.roleRef.name, transaction) + if (!roleWithRules) { + throw new Errors.ValidationError(`Referenced role '${saData.roleRef.name}' does not exist`) + } + // System role - roleId will be null (system roles don't have DB ids) + } + + let serviceAccount + try { + serviceAccount = await this.create({ + name: saData.name, + // namespace removed (controller manages single namespace) + roleRef: saData.roleRef, + roleId: role ? role.id : null + }, transaction) + } catch (error) { + // Handle SequelizeUniqueConstraintError as a safety net + if (error.name === 'SequelizeUniqueConstraintError') { + throw new Errors.ConflictError(`ServiceAccount '${saData.name}' already exists`) + } + throw error + } + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return serviceAccount + } + + /** + * Update a ServiceAccount + */ + async updateServiceAccount (name, saData, transaction) { + const sa = await this.findOne({ name }, transaction) + if (!sa) { + throw new Errors.NotFoundError(`ServiceAccount '${name}' not found`) + } + + // Validate roleRef is provided if updating and get role ID + let roleId = null + if (saData.roleRef !== undefined) { + if (!saData.roleRef || !saData.roleRef.name) { + throw new Errors.ValidationError('ServiceAccount roleRef must have a name.') + } + + // Validate role reference exists - use findOne to get the actual model with id + const role = await RbacRoleManager.findOne({ name: saData.roleRef.name }, transaction) + if (!role) { + // Check if it's a system role + const roleWithRules = await RbacRoleManager.getRoleWithRules(saData.roleRef.name, transaction) + if (!roleWithRules) { + throw new Errors.ValidationError(`Referenced role '${saData.roleRef.name}' does not exist`) + } + // System role - roleId will be null (system roles don't have DB ids) + } else { + roleId = role.id + } + } else { + // If roleRef is not provided, keep existing roleId + const existingRoleRef = sa.roleRef + if (existingRoleRef && existingRoleRef.name) { + const role = await RbacRoleManager.findOne({ name: existingRoleRef.name }, transaction) + if (role) { + roleId = role.id + } + // If not found in DB, it might be a system role - keep roleId as null + } + } + + const updateData = {} + if (saData.name) updateData.name = saData.name + // namespace removed (not stored in database) + if (saData.roleRef !== undefined) updateData.roleRef = saData.roleRef + if (roleId !== null) updateData.roleId = roleId + + await this.update({ name }, updateData, transaction) + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return this.findOne({ name: saData.name || name }, transaction) + } + + /** + * Delete a ServiceAccount + */ + async deleteServiceAccount (name, transaction) { + const sa = await this.findOne({ name }, transaction) + if (!sa) { + throw new Errors.NotFoundError(`ServiceAccount '${name}' not found`) + } + + await this.delete({ name }, transaction) + + // Increment cache version to invalidate caches on all instances + await RbacCacheVersionManager.incrementVersion(transaction) + + return { message: `ServiceAccount '${name}' deleted successfully` } + } + + /** + * Get a ServiceAccount by name + */ + async getServiceAccount (name, transaction) { + return this.findOne({ name }, transaction) + } + + /** + * List all ServiceAccounts + */ + async listServiceAccounts (transaction) { + return this.findAll({}, transaction) + } +} + +module.exports = new RbacServiceAccountManager() diff --git a/src/data/managers/registry-manager.js b/src/data/managers/registry-manager.js index 0d8864d7..4bd066fd 100644 --- a/src/data/managers/registry-manager.js +++ b/src/data/managers/registry-manager.js @@ -1,4 +1,6 @@ const BaseManager = require('./base-manager') +const SecretHelper = require('../../helpers/secret-helper') +const vaultManager = require('../../vault/vault-manager') const models = require('../models') const Registry = models.Registry @@ -6,6 +8,18 @@ class RegistryManager extends BaseManager { getEntity () { return Registry } + + async delete (data, transaction) { + const registry = await this.findOne(data || {}, transaction) + if (registry && vaultManager.isEnabled()) { + try { + await SecretHelper.deleteSecret('registry-' + registry.id, 'registry') + } catch (err) { + // Ignore 404 or other errors (e.g. password was never stored in vault) + } + } + return super.delete(data, transaction) + } } const instance = new RegistryManager() diff --git a/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql b/src/data/migrations/mysql/db_migration_mysql_v1.1.0.sql similarity index 91% rename from src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql rename to src/data/migrations/mysql/db_migration_mysql_v1.1.0.sql index 4fe961fb..36d9ba36 100644 --- a/src/data/migrations/mysql/db_migration_mysql_v1.0.7.sql +++ b/src/data/migrations/mysql/db_migration_mysql_v1.1.0.sql @@ -881,4 +881,85 @@ ALTER TABLE ChangeTrackings ADD COLUMN microservice_logs BOOLEAN DEFAULT false; ALTER TABLE ChangeTrackings ADD COLUMN fog_logs BOOLEAN DEFAULT false; +CREATE TABLE IF NOT EXISTS RbacRoles ( + id INT AUTO_INCREMENT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + kind TEXT DEFAULT 'Role', + created_at DATETIME, + updated_at DATETIME, + UNIQUE KEY unique_name (name(255)) +); + +CREATE TABLE IF NOT EXISTS RbacRoleRules ( + id INT AUTO_INCREMENT PRIMARY KEY, + role_id INT NOT NULL, + api_groups TEXT NOT NULL, + resources TEXT NOT NULL, + verbs TEXT NOT NULL, + resource_names TEXT, + created_at DATETIME, + updated_at DATETIME, + FOREIGN KEY (role_id) REFERENCES RbacRoles (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS RbacRoleBindings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + kind TEXT DEFAULT 'RoleBinding', + role_ref TEXT NOT NULL, + subjects TEXT NOT NULL, + created_at DATETIME, + updated_at DATETIME, + UNIQUE KEY unique_name (name(255)) +); + +CREATE TABLE IF NOT EXISTS RbacServiceAccounts ( + id INT AUTO_INCREMENT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + role_ref TEXT, + created_at DATETIME, + updated_at DATETIME, + UNIQUE KEY unique_name (name(255)) +); + +CREATE INDEX idx_rbac_role_rules_role_id ON RbacRoleRules (role_id); +CREATE INDEX idx_rbac_roles_name ON RbacRoles (name(255)); +CREATE INDEX idx_rbac_role_bindings_name ON RbacRoleBindings (name(255)); +CREATE INDEX idx_rbac_service_accounts_name ON RbacServiceAccounts (name(255)); + +CREATE TABLE IF NOT EXISTS RbacCacheVersion ( + id INT PRIMARY KEY DEFAULT 1, + version BIGINT NOT NULL DEFAULT 1, + created_at DATETIME, + updated_at DATETIME, + CONSTRAINT single_row CHECK (id = 1) +); + + +ALTER TABLE Microservices ADD COLUMN service_account_id INT; +CREATE INDEX idx_microservices_service_account_id ON Microservices (service_account_id); +ALTER TABLE Microservices ADD CONSTRAINT fk_microservices_service_account_id FOREIGN KEY (service_account_id) REFERENCES RbacServiceAccounts (id); + +ALTER TABLE RbacRoleBindings ADD COLUMN role_id INT; +CREATE INDEX idx_rbac_role_bindings_role_id ON RbacRoleBindings (role_id); +ALTER TABLE RbacRoleBindings ADD CONSTRAINT fk_rbac_role_bindings_role_id FOREIGN KEY (role_id) REFERENCES RbacRoles (id); + +ALTER TABLE RbacServiceAccounts ADD COLUMN role_id INT; +CREATE INDEX idx_rbac_service_accounts_role_id ON RbacServiceAccounts (role_id); +ALTER TABLE RbacServiceAccounts ADD CONSTRAINT fk_rbac_service_accounts_role_id FOREIGN KEY (role_id) REFERENCES RbacRoles (id); + +CREATE TABLE IF NOT EXISTS ClusterControllers ( + uuid VARCHAR(36) PRIMARY KEY NOT NULL, + host VARCHAR(255), + process_id INT, + last_heartbeat DATETIME, + is_active BOOLEAN DEFAULT true, + created_at DATETIME, + updated_at DATETIME +); + +CREATE INDEX idx_cluster_controllers_uuid ON ClusterControllers (uuid); +CREATE INDEX idx_cluster_controllers_host ON ClusterControllers (host); +CREATE INDEX idx_cluster_controllers_active ON ClusterControllers (is_active, last_heartbeat); + COMMIT; \ No newline at end of file diff --git a/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql b/src/data/migrations/postgres/db_migration_pg_v1.1.0.sql similarity index 91% rename from src/data/migrations/postgres/db_migration_pg_v1.0.7.sql rename to src/data/migrations/postgres/db_migration_pg_v1.1.0.sql index b53ffc86..b29acc69 100644 --- a/src/data/migrations/postgres/db_migration_pg_v1.0.7.sql +++ b/src/data/migrations/postgres/db_migration_pg_v1.1.0.sql @@ -881,3 +881,79 @@ CREATE INDEX idx_fog_log_status_session_id ON "FogLogStatuses" (session_id); ALTER TABLE "ChangeTrackings" ADD COLUMN microservice_logs BOOLEAN DEFAULT false; ALTER TABLE "ChangeTrackings" ADD COLUMN fog_logs BOOLEAN DEFAULT false; +CREATE TABLE IF NOT EXISTS "RbacRoles" ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + kind TEXT DEFAULT 'Role', + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0) +); + +CREATE TABLE IF NOT EXISTS "RbacRoleRules" ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + role_id INT NOT NULL, + api_groups TEXT NOT NULL, + resources TEXT NOT NULL, + verbs TEXT NOT NULL, + resource_names TEXT, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0), + FOREIGN KEY (role_id) REFERENCES "RbacRoles" (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS "RbacRoleBindings" ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + kind TEXT DEFAULT 'RoleBinding', + role_ref TEXT NOT NULL, + subjects TEXT NOT NULL, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0) +); + +CREATE TABLE IF NOT EXISTS "RbacServiceAccounts" ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + role_ref TEXT, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0) +); + +CREATE INDEX idx_rbac_role_rules_role_id ON "RbacRoleRules" (role_id); +CREATE INDEX idx_rbac_roles_name ON "RbacRoles" (name); +CREATE INDEX idx_rbac_role_bindings_name ON "RbacRoleBindings" (name); +CREATE INDEX idx_rbac_service_accounts_name ON "RbacServiceAccounts" (name); + +CREATE TABLE IF NOT EXISTS "RbacCacheVersion" ( + id INT PRIMARY KEY DEFAULT 1, + version BIGINT NOT NULL DEFAULT 1, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0), + CONSTRAINT single_row CHECK (id = 1) +); + +ALTER TABLE "Microservices" ADD COLUMN service_account_id INT; +CREATE INDEX idx_microservices_service_account_id ON "Microservices" (service_account_id); +ALTER TABLE "Microservices" ADD CONSTRAINT fk_microservices_service_account_id FOREIGN KEY (service_account_id) REFERENCES "RbacServiceAccounts" (id); + +ALTER TABLE "RbacRoleBindings" ADD COLUMN role_id INT; +CREATE INDEX idx_rbac_role_bindings_role_id ON "RbacRoleBindings" (role_id); +ALTER TABLE "RbacRoleBindings" ADD CONSTRAINT fk_rbac_role_bindings_role_id FOREIGN KEY (role_id) REFERENCES "RbacRoles" (id); + +ALTER TABLE "RbacServiceAccounts" ADD COLUMN role_id INT; +CREATE INDEX idx_rbac_service_accounts_role_id ON "RbacServiceAccounts" (role_id); +ALTER TABLE "RbacServiceAccounts" ADD CONSTRAINT fk_rbac_service_accounts_role_id FOREIGN KEY (role_id) REFERENCES "RbacRoles" (id); + +CREATE TABLE IF NOT EXISTS "ClusterControllers" ( + uuid VARCHAR(36) PRIMARY KEY NOT NULL, + host VARCHAR(255), + process_id INT, + last_heartbeat TIMESTAMP(0), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP(0), + updated_at TIMESTAMP(0) +); + +CREATE INDEX idx_cluster_controllers_uuid ON "ClusterControllers" (uuid); +CREATE INDEX idx_cluster_controllers_host ON "ClusterControllers" (host); +CREATE INDEX idx_cluster_controllers_active ON "ClusterControllers" (is_active, last_heartbeat); \ No newline at end of file diff --git a/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql b/src/data/migrations/sqlite/db_migration_sqlite_v1.1.0.sql similarity index 92% rename from src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql rename to src/data/migrations/sqlite/db_migration_sqlite_v1.1.0.sql index b69097cd..645114f8 100644 --- a/src/data/migrations/sqlite/db_migration_sqlite_v1.0.7.sql +++ b/src/data/migrations/sqlite/db_migration_sqlite_v1.1.0.sql @@ -866,3 +866,78 @@ CREATE INDEX idx_fog_log_status_session_id ON FogLogStatuses (session_id); ALTER TABLE ChangeTrackings ADD COLUMN microservice_logs BOOLEAN DEFAULT false; ALTER TABLE ChangeTrackings ADD COLUMN fog_logs BOOLEAN DEFAULT false; + + +CREATE TABLE IF NOT EXISTS RbacRoles ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL, + kind TEXT DEFAULT 'Role', + created_at DATETIME, + updated_at DATETIME +); + + +CREATE TABLE IF NOT EXISTS RbacRoleRules ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + role_id INTEGER NOT NULL, + api_groups TEXT NOT NULL, + resources TEXT NOT NULL, + verbs TEXT NOT NULL, + resource_names TEXT, + created_at DATETIME, + updated_at DATETIME, + FOREIGN KEY (role_id) REFERENCES RbacRoles (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS RbacRoleBindings ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL, + kind TEXT DEFAULT 'RoleBinding', + role_ref TEXT NOT NULL, + subjects TEXT NOT NULL, + created_at DATETIME, + updated_at DATETIME +); + +CREATE TABLE IF NOT EXISTS RbacServiceAccounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL, + role_ref TEXT, + created_at DATETIME, + updated_at DATETIME +); + +CREATE INDEX idx_rbac_role_rules_role_id ON RbacRoleRules (role_id); +CREATE INDEX idx_rbac_roles_name ON RbacRoles (name); +CREATE INDEX idx_rbac_role_bindings_name ON RbacRoleBindings (name); +CREATE INDEX idx_rbac_service_accounts_name ON RbacServiceAccounts (name); + +CREATE TABLE IF NOT EXISTS RbacCacheVersion ( + id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), + version INTEGER NOT NULL DEFAULT 1, + created_at DATETIME, + updated_at DATETIME +); + +ALTER TABLE Microservices ADD COLUMN service_account_id INTEGER; +CREATE INDEX idx_microservices_service_account_id ON Microservices (service_account_id); + +ALTER TABLE RbacRoleBindings ADD COLUMN role_id INTEGER; +CREATE INDEX idx_rbac_role_bindings_role_id ON RbacRoleBindings (role_id); + +ALTER TABLE RbacServiceAccounts ADD COLUMN role_id INTEGER; +CREATE INDEX idx_rbac_service_accounts_role_id ON RbacServiceAccounts (role_id); + +CREATE TABLE IF NOT EXISTS ClusterControllers ( + uuid VARCHAR(36) PRIMARY KEY NOT NULL, + host VARCHAR(255), + process_id INTEGER, + last_heartbeat DATETIME, + is_active BOOLEAN DEFAULT true, + created_at DATETIME, + updated_at DATETIME +); + +CREATE INDEX idx_cluster_controllers_uuid ON ClusterControllers (uuid); +CREATE INDEX idx_cluster_controllers_host ON ClusterControllers (host); +CREATE INDEX idx_cluster_controllers_active ON ClusterControllers (is_active, last_heartbeat); \ No newline at end of file diff --git a/src/data/models/cluster-controller.js b/src/data/models/cluster-controller.js new file mode 100644 index 00000000..5524e525 --- /dev/null +++ b/src/data/models/cluster-controller.js @@ -0,0 +1,37 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const ClusterController = sequelize.define('ClusterController', { + uuid: { + type: DataTypes.STRING(36), + primaryKey: true, + allowNull: false, + field: 'uuid' + }, + host: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'host' + }, + processId: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'process_id' + }, + lastHeartbeat: { + type: DataTypes.DATE, + allowNull: true, + field: 'last_heartbeat' + }, + isActive: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + field: 'is_active' + } + }, { + tableName: 'ClusterControllers', + timestamps: true, + underscored: true + }) + return ClusterController +} diff --git a/src/data/models/index.js b/src/data/models/index.js index 4eb51ae1..1aa27695 100644 --- a/src/data/models/index.js +++ b/src/data/models/index.js @@ -68,10 +68,30 @@ db.initDB = async (isStart) => { await databaseProvider.runSeederPostgres(databaseProvider.sequelize) } + // Initialize RBAC cache version if it doesn't exist + try { + const RbacCacheVersionManager = require('../managers/rbac-cache-version-manager') + const fakeTransaction = { fakeTransaction: true } + await RbacCacheVersionManager.initializeVersion(fakeTransaction) + logger.info('RBAC cache version initialized') + } catch (error) { + logger.warn(`Failed to initialize RBAC cache version: ${error.message}. Continuing...`) + } + // Configure system images const fogTypes = await db.FogType.findAll({}) await configureImage(db, constants.ROUTER_CATALOG_NAME, fogTypes, config.get('systemImages.router', {})) await configureImage(db, constants.DEBUG_CATALOG_NAME, fogTypes, config.get('systemImages.debug', {})) + + // Initialize controller UUID + try { + const ClusterControllerService = require('../../services/cluster-controller-service') + const fakeTransaction = { fakeTransaction: true } + await ClusterControllerService.initializeControllerUuid(fakeTransaction) + logger.info('Controller UUID initialized') + } catch (error) { + logger.warn(`Failed to initialize controller UUID: ${error.message}. Continuing...`) + } } } diff --git a/src/data/models/microservice.js b/src/data/models/microservice.js index 8278d10e..c8fde4f3 100644 --- a/src/data/models/microservice.js +++ b/src/data/models/microservice.js @@ -118,6 +118,11 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.BOOLEAN, field: 'is_activated', defaultValue: true + }, + serviceAccountId: { + type: DataTypes.INTEGER, + field: 'service_account_id', + allowNull: true } }, { tableName: 'Microservices', @@ -234,6 +239,14 @@ module.exports = (sequelize, DataTypes) => { Microservice.belongsToMany(models.Tags, { as: 'pubTags', through: 'MicroservicePubTags' }) Microservice.belongsToMany(models.Tags, { as: 'subTags', through: 'MicroserviceSubTags' }) + + Microservice.belongsTo(models.RbacServiceAccount, { + foreignKey: { + name: 'serviceAccountId', + field: 'service_account_id' + }, + as: 'serviceAccount' + }) } return Microservice diff --git a/src/data/models/rbacCacheVersion.js b/src/data/models/rbacCacheVersion.js new file mode 100644 index 00000000..a1161d65 --- /dev/null +++ b/src/data/models/rbacCacheVersion.js @@ -0,0 +1,31 @@ +'use strict' + +module.exports = (sequelize, DataTypes) => { + const RbacCacheVersion = sequelize.define('RbacCacheVersion', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + defaultValue: 1, + field: 'id' + }, + version: { + type: DataTypes.BIGINT, + allowNull: false, + defaultValue: 1, + field: 'version' + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: true, + field: 'updated_at' + } + }, { + tableName: 'RbacCacheVersion', + timestamps: true, + underscored: true, + freezeTableName: true + }) + + return RbacCacheVersion +} diff --git a/src/data/models/rbacRole.js b/src/data/models/rbacRole.js new file mode 100644 index 00000000..6ebefb27 --- /dev/null +++ b/src/data/models/rbacRole.js @@ -0,0 +1,48 @@ +'use strict' + +module.exports = (sequelize, DataTypes) => { + const RbacRole = sequelize.define('RbacRole', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'id' + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + field: 'name', + unique: true + }, + kind: { + type: DataTypes.TEXT, + allowNull: false, + field: 'kind', + defaultValue: 'Role' + } + }, { + tableName: 'RbacRoles', + timestamps: true, + underscored: true, + indexes: [ + { + unique: true, + fields: ['name'] + } + ] + }) + + RbacRole.associate = function (models) { + RbacRole.hasMany(models.RbacRoleRule, { + foreignKey: { + name: 'roleId', + field: 'role_id' + }, + as: 'rules', + onDelete: 'CASCADE' + }) + } + + return RbacRole +} diff --git a/src/data/models/rbacRoleBinding.js b/src/data/models/rbacRoleBinding.js new file mode 100644 index 00000000..f37158e6 --- /dev/null +++ b/src/data/models/rbacRoleBinding.js @@ -0,0 +1,86 @@ +'use strict' + +module.exports = (sequelize, DataTypes) => { + const RbacRoleBinding = sequelize.define('RbacRoleBinding', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'id' + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + field: 'name', + unique: true + }, + kind: { + type: DataTypes.TEXT, + allowNull: false, + field: 'kind', + defaultValue: 'RoleBinding' + }, + roleRef: { + type: DataTypes.TEXT, + allowNull: false, + field: 'role_ref', + get () { + const rawValue = this.getDataValue('roleRef') + if (!rawValue) return {} + try { + return JSON.parse(rawValue) + } catch (err) { + return {} + } + }, + set (value) { + this.setDataValue('roleRef', JSON.stringify(value)) + } + }, + roleId: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'role_id' + }, + subjects: { + type: DataTypes.TEXT, + allowNull: false, + field: 'subjects', + get () { + const rawValue = this.getDataValue('subjects') + if (!rawValue) return [] + try { + return JSON.parse(rawValue) + } catch (err) { + return [] + } + }, + set (value) { + this.setDataValue('subjects', JSON.stringify(value)) + } + } + }, { + tableName: 'RbacRoleBindings', + timestamps: true, + underscored: true, + indexes: [ + { + unique: true, + fields: ['name'] + } + ] + }) + + RbacRoleBinding.associate = function (models) { + RbacRoleBinding.belongsTo(models.RbacRole, { + foreignKey: { + name: 'roleId', + field: 'role_id' + }, + as: 'role' + }) + } + + return RbacRoleBinding +} diff --git a/src/data/models/rbacRoleRule.js b/src/data/models/rbacRoleRule.js new file mode 100644 index 00000000..7b762e77 --- /dev/null +++ b/src/data/models/rbacRoleRule.js @@ -0,0 +1,112 @@ +'use strict' + +module.exports = (sequelize, DataTypes) => { + const RbacRoleRule = sequelize.define('RbacRoleRule', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'id' + }, + roleId: { + type: DataTypes.INTEGER, + allowNull: false, + field: 'role_id' + }, + apiGroups: { + type: DataTypes.TEXT, + allowNull: false, + field: 'api_groups', + get () { + const rawValue = this.getDataValue('apiGroups') + if (!rawValue) return [] + try { + return JSON.parse(rawValue) + } catch (err) { + return [] + } + }, + set (value) { + this.setDataValue('apiGroups', JSON.stringify(value)) + } + }, + resources: { + type: DataTypes.TEXT, + allowNull: false, + field: 'resources', + get () { + const rawValue = this.getDataValue('resources') + if (!rawValue) return [] + try { + return JSON.parse(rawValue) + } catch (err) { + return [] + } + }, + set (value) { + this.setDataValue('resources', JSON.stringify(value)) + } + }, + verbs: { + type: DataTypes.TEXT, + allowNull: false, + field: 'verbs', + get () { + const rawValue = this.getDataValue('verbs') + if (!rawValue) return [] + try { + return JSON.parse(rawValue) + } catch (err) { + return [] + } + }, + set (value) { + this.setDataValue('verbs', JSON.stringify(value)) + } + }, + resourceNames: { + type: DataTypes.TEXT, + allowNull: true, + field: 'resource_names', + get () { + const rawValue = this.getDataValue('resourceNames') + if (!rawValue) return null + try { + return JSON.parse(rawValue) + } catch (err) { + return null + } + }, + set (value) { + if (value === null || value === undefined) { + this.setDataValue('resourceNames', null) + } else { + this.setDataValue('resourceNames', JSON.stringify(value)) + } + } + } + }, { + tableName: 'RbacRoleRules', + timestamps: true, + underscored: true, + indexes: [ + { + fields: ['role_id'] + } + ] + }) + + RbacRoleRule.associate = function (models) { + RbacRoleRule.belongsTo(models.RbacRole, { + foreignKey: { + name: 'roleId', + field: 'role_id' + }, + as: 'role', + onDelete: 'CASCADE' + }) + } + + return RbacRoleRule +} diff --git a/src/data/models/rbacServiceAccount.js b/src/data/models/rbacServiceAccount.js new file mode 100644 index 00000000..e693efa5 --- /dev/null +++ b/src/data/models/rbacServiceAccount.js @@ -0,0 +1,67 @@ +'use strict' + +module.exports = (sequelize, DataTypes) => { + const RbacServiceAccount = sequelize.define('RbacServiceAccount', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + field: 'id' + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + field: 'name', + unique: true + }, + roleRef: { + type: DataTypes.TEXT, + allowNull: false, + field: 'role_ref', + get () { + const rawValue = this.getDataValue('roleRef') + if (!rawValue) return null + try { + return JSON.parse(rawValue) + } catch (err) { + return null + } + }, + set (value) { + if (value === null || value === undefined) { + this.setDataValue('roleRef', null) + } else { + this.setDataValue('roleRef', JSON.stringify(value)) + } + } + }, + roleId: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'role_id' + } + }, { + tableName: 'RbacServiceAccounts', + timestamps: true, + underscored: true, + indexes: [ + { + unique: true, + fields: ['name'] + } + ] + }) + + RbacServiceAccount.associate = function (models) { + RbacServiceAccount.belongsTo(models.RbacRole, { + foreignKey: { + name: 'roleId', + field: 'role_id' + }, + as: 'role' + }) + } + + return RbacServiceAccount +} diff --git a/src/data/models/registry.js b/src/data/models/registry.js index fe169e51..93f05c8d 100644 --- a/src/data/models/registry.js +++ b/src/data/models/registry.js @@ -1,4 +1,25 @@ 'use strict' + +const SecretHelper = require('../../helpers/secret-helper') + +// Minimum length for internal encryption format: base64(salt(16) + iv(12) + tag(16) + encrypted) +const INTERNAL_ENCRYPTED_MIN_LENGTH = 60 + +function isPasswordEmpty (password) { + return password == null || (typeof password === 'string' && password.trim() === '') +} + +function looksLikeInternalEncrypted (password) { + if (password == null || typeof password !== 'string') return false + if (password.length < INTERNAL_ENCRYPTED_MIN_LENGTH) return false + try { + const buf = Buffer.from(password, 'base64') + return buf.length >= 44 // salt + iv + tag minimum + } catch (err) { + return false + } +} + module.exports = (sequelize, DataTypes) => { const Registry = sequelize.define('Registry', { id: { @@ -22,7 +43,16 @@ module.exports = (sequelize, DataTypes) => { }, password: { type: DataTypes.TEXT, - field: 'password' + field: 'password', + get () { + const rawValue = this.getDataValue('password') + if (rawValue == null) return rawValue + if (SecretHelper.isVaultReference(rawValue)) return rawValue + return rawValue + }, + set (value) { + this.setDataValue('password', value) + } }, userEmail: { type: DataTypes.TEXT, @@ -31,7 +61,55 @@ module.exports = (sequelize, DataTypes) => { }, { tableName: 'Registries', timestamps: false, - underscored: true + underscored: true, + hooks: { + beforeSave: async (registry) => { + if (!registry.changed('password')) return + const password = registry.password + if (isPasswordEmpty(password)) { + registry.password = '' + return + } + if (!registry.id) { + return + } + if (SecretHelper.isVaultReference(password) || looksLikeInternalEncrypted(password)) { + return + } + const encrypted = await SecretHelper.encryptSecret( + { value: password }, + 'registry-' + registry.id, + 'registry' + ) + registry.password = encrypted + }, + afterFind: async (result) => { + const decryptPassword = async (registry) => { + if (!registry || registry.password == null) return + if (isPasswordEmpty(registry.password)) return + if (!registry.id) return + try { + const decrypted = await SecretHelper.decryptSecret( + registry.password, + 'registry-' + registry.id, + 'registry' + ) + registry.password = decrypted && typeof decrypted.value !== 'undefined' + ? decrypted.value + : registry.password + } catch (error) { + // Legacy plain password or error - leave unchanged + } + } + if (Array.isArray(result)) { + for (const registry of result) { + await decryptPassword(registry) + } + } else { + await decryptPassword(result) + } + } + } }) return Registry } diff --git a/src/data/providers/database-provider.js b/src/data/providers/database-provider.js index 6d1ce48c..70b4414c 100644 --- a/src/data/providers/database-provider.js +++ b/src/data/providers/database-provider.js @@ -251,8 +251,8 @@ class DatabaseProvider { // SQLite migration async runMigrationSQLite (dbName) { - const migrationSqlPath = path.resolve(__dirname, '../migrations/sqlite/db_migration_sqlite_v1.0.7.sql') - const migrationVersion = '1.0.7' + const migrationSqlPath = path.resolve(__dirname, '../migrations/sqlite/db_migration_sqlite_v1.1.0.sql') + const migrationVersion = '1.0.9' if (!fs.existsSync(migrationSqlPath)) { logger.error(`Migration file not found: ${migrationSqlPath}`) @@ -324,8 +324,8 @@ class DatabaseProvider { // MySQL migration async runMigrationMySQL (db) { - const migrationSqlPath = path.resolve(__dirname, '../migrations/mysql/db_migration_mysql_v1.0.7.sql') - const migrationVersion = '1.0.7' + const migrationSqlPath = path.resolve(__dirname, '../migrations/mysql/db_migration_mysql_v1.1.0.sql') + const migrationVersion = '1.0.9' if (!fs.existsSync(migrationSqlPath)) { logger.error(`Migration file not found: ${migrationSqlPath}`) @@ -385,8 +385,8 @@ class DatabaseProvider { // PostgreSQL migration async runMigrationPostgres (db) { - const migrationSqlPath = path.resolve(__dirname, '../migrations/postgres/db_migration_pg_v1.0.7.sql') - const migrationVersion = '1.0.7' + const migrationSqlPath = path.resolve(__dirname, '../migrations/postgres/db_migration_pg_v1.1.0.sql') + const migrationVersion = '1.0.9' if (!fs.existsSync(migrationSqlPath)) { logger.error(`Migration file not found: ${migrationSqlPath}`) diff --git a/src/helpers/error-messages.js b/src/helpers/error-messages.js index 9e1b8536..6e1cfad3 100644 --- a/src/helpers/error-messages.js +++ b/src/helpers/error-messages.js @@ -80,6 +80,9 @@ module.exports = { USER_ALREADY_ACTIVATED: 'User is already activated.', USER_NOT_ACTIVATED_YET: 'User is not activated yet.', REGISTRY_IS_SYSTEM: 'Registry is system and can\'t be updated or deleted', + INVALID_CLUSTER_CONTROLLER_UUID: 'Invalid cluster controller UUID \'{}\'', + CLUSTER_CONTROLLER_NOT_FOUND: 'Cluster controller not found', + CLUSTER_CONTROLLER_REGISTRATION_FAILED: 'Cluster controller registration failed', REGISTRY_IS_IN_USE: 'Registry is in use by microservices and can\'t be deleted', CLI: { INVALID_PORT_MAPPING: 'Port mapping parsing error. Please provide valid port mapping.', diff --git a/src/jobs/controller-cleanup-job.js b/src/jobs/controller-cleanup-job.js new file mode 100644 index 00000000..449ac4ef --- /dev/null +++ b/src/jobs/controller-cleanup-job.js @@ -0,0 +1,71 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const ClusterControllerManager = require('../data/managers/cluster-controller-manager') +const Config = require('../config') +const logger = require('../logger') +const Sequelize = require('sequelize') +const Op = Sequelize.Op + +async function run () { + try { + await cleanupInactiveControllers() + } catch (error) { + logger.error('Error during controller cleanup:', error) + } finally { + // Schedule next run with current interval (may have changed via env var) + const currentInterval = process.env.CONTROLLER_CLEANUP_INTERVAL || Config.get('settings.controllerCleanupInterval', 600) + setTimeout(run, currentInterval * 1000) + } +} + +async function cleanupInactiveControllers () { + try { + const thresholdSeconds = process.env.CONTROLLER_INACTIVE_THRESHOLD || Config.get('settings.controllerInactiveThreshold', 300) + const threshold = new Date(Date.now() - thresholdSeconds * 1000) + + logger.debug(`Starting cleanup of controllers inactive for more than ${thresholdSeconds} seconds`) + + const fakeTransaction = { fakeTransaction: true } + const inactive = await ClusterControllerManager.findAll({ + isActive: true, + lastHeartbeat: { [Op.lt]: threshold } + }, fakeTransaction) + + let cleanedCount = 0 + for (const controller of inactive) { + await ClusterControllerManager.update( + { uuid: controller.uuid }, + { isActive: false }, + fakeTransaction + ) + logger.info(`Marked controller ${controller.uuid} on host ${controller.host} as inactive (last heartbeat: ${controller.lastHeartbeat})`) + cleanedCount++ + } + + if (cleanedCount > 0) { + logger.info(`Cleaned up ${cleanedCount} inactive controller(s)`) + } else { + logger.debug('No inactive controllers to clean up') + } + + return cleanedCount + } catch (error) { + logger.error('Error during controller cleanup:', error) + throw error + } +} + +module.exports = { + run +} diff --git a/src/jobs/controller-heartbeat-job.js b/src/jobs/controller-heartbeat-job.js new file mode 100644 index 00000000..3b960733 --- /dev/null +++ b/src/jobs/controller-heartbeat-job.js @@ -0,0 +1,49 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const ClusterControllerService = require('../services/cluster-controller-service') +const Config = require('../config') +const logger = require('../logger') + +const scheduleTime = (Config.get('settings.controllerHeartbeatInterval', 30)) * 1000 + +async function run () { + try { + await updateControllerHeartbeat() + } catch (error) { + logger.error('Error during controller heartbeat update:', error) + } finally { + setTimeout(run, scheduleTime) + } +} + +async function updateControllerHeartbeat () { + try { + const uuid = ClusterControllerService.getCurrentControllerUuid() + if (!uuid) { + logger.debug('Controller UUID not initialized yet, skipping heartbeat') + return + } + + const fakeTransaction = { fakeTransaction: true } + await ClusterControllerService.updateHeartbeat(uuid, fakeTransaction) + logger.debug(`Updated heartbeat for controller: ${uuid}`) + } catch (error) { + logger.error(`Failed to update controller heartbeat: ${error.message}`) + throw error + } +} + +module.exports = { + run +} diff --git a/src/lib/rbac/authorizer.js b/src/lib/rbac/authorizer.js new file mode 100644 index 00000000..87faaedb --- /dev/null +++ b/src/lib/rbac/authorizer.js @@ -0,0 +1,264 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const RbacRoleBindingManager = require('../../data/managers/rbac-role-binding-manager') +const RbacRoleManager = require('../../data/managers/rbac-role-manager') +const RbacCacheVersionManager = require('../../data/managers/rbac-cache-version-manager') +const logger = require('../../logger') + + +// Simple in-memory cache for authorization decisions +// Key format: `${subjectKind}:${subjectName}:${apiGroup}:${resource}:${verb}:${resourceName}` +const authCache = new Map() +const CACHE_TTL = 5 * 60 * 1000 // 5 minutes +const MAX_CACHE_SIZE = 10000 + +// Track last known cache version to detect changes across instances +let lastKnownVersion = null + +/** + * Check if a value matches a pattern (supports wildcard *) + */ +function matchesPattern (value, pattern) { + if (pattern === '*' || pattern === value) { + return true + } + // Simple wildcard matching + if (pattern.includes('*')) { + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') + return regex.test(value) + } + return false +} + +/** + * Check if a value is in an array or matches wildcard + */ +function matchesArray (value, array) { + if (!array || array.length === 0) { + return false + } + return array.some(item => matchesPattern(value, item)) +} + +/** + * Resolve all rules for a subject + */ +async function resolveRulesForSubject (subject, transaction) { + const bindings = await RbacRoleBindingManager.findRoleBindingsBySubject(subject, transaction) + const allRules = [] + + for (const binding of bindings) { + const roleRef = binding.roleRef + if (roleRef && roleRef.kind === 'Role' && roleRef.name) { + // getRoleWithRules handles both system roles (static) and database roles + const role = await RbacRoleManager.getRoleWithRules(roleRef.name, transaction) + if (role && role.rules) { + allRules.push(...role.rules) + } + } + } + + return allRules +} + +/** + * Evaluate if a rule allows the requested action + */ +function evaluateRule (rule, apiGroup, resource, verb, resourceName) { + // Check apiGroups + const apiGroups = rule.apiGroups || [''] + if (!matchesArray(apiGroup || '', apiGroups)) { + return false + } + + // Check resources + const resources = rule.resources || [] + if (!matchesArray(resource, resources)) { + return false + } + + // Check verbs + const verbs = rule.verbs || [] + if (!matchesArray(verb, verbs)) { + return false + } + + // Check resourceNames if specified + if (rule.resourceNames && rule.resourceNames.length > 0) { + if (!resourceName || !matchesArray(resourceName, rule.resourceNames)) { + return false + } + } + + return true +} + +/** + * Authorize a request + * @param {Array} subjects - Array of subjects {kind, name} + * @param {string} apiGroup - API group (empty string for core) + * @param {string} resource - Resource name (e.g., 'microservices') + * @param {string} verb - Verb (e.g., 'get', 'create', 'patch') + * @param {string} resourceName - Optional resource instance name (e.g., microservice UUID) + * @param {Object} transaction - Database transaction + * @returns {Promise<{allowed: boolean, reason?: string}>} + */ +async function authorize (subjects, apiGroup, resource, verb, resourceName, transaction) { + if (!subjects || !Array.isArray(subjects) || subjects.length === 0) { + return { allowed: false, reason: 'No subjects provided' } + } + + // Check if cache version has changed (for multi-instance cache invalidation) + try { + const currentVersion = await RbacCacheVersionManager.getVersion(transaction) + if (lastKnownVersion !== null && currentVersion !== lastKnownVersion) { + // Cache version changed - clear local cache + logger.info('Cache version changed - clearing cache') + authCache.clear() + } + lastKnownVersion = currentVersion + } catch (error) { + // Log error but continue - if version check fails, we'll just skip cache version check + logger.warn(`Error checking cache version: ${error.message}`) + } + + // Check cache + const cacheKey = `${JSON.stringify(subjects)}:${apiGroup}:${resource}:${verb}:${resourceName || ''}` + const cached = authCache.get(cacheKey) + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.result + } + + // Check system roles first (Admin, SRE, Developer, Viewer) + // These work directly without RoleBindings when Keycloak role name matches + for (const subject of subjects) { + if (subject.kind === 'Group' && subject.name) { + const roleName = subject.name.toLowerCase() + + // Check if it matches a system role + if (RbacRoleManager.isSystemRole(roleName)) { + const systemRole = RbacRoleManager.getSystemRole(roleName) + if (systemRole && systemRole.rules && Array.isArray(systemRole.rules)) { + // Check if the system role's rules allow this action + const rules = systemRole.rules + for (const rule of rules) { + try { + if (evaluateRule(rule, apiGroup, resource, verb, resourceName)) { + const result = { allowed: true, reason: `${roleName} system role has permission` } + // Cache result + if (authCache.size < MAX_CACHE_SIZE) { + authCache.set(cacheKey, { result, timestamp: Date.now() }) + } + return result + } + } catch (ruleError) { + // Log error but continue to next rule + logger.error('Error evaluating rule:', JSON.stringify({ + error: ruleError.message, + stack: ruleError.stack, + rule: rule, + apiGroup, + resource, + verb, + resourceName + })) + } + } + } + } + } + } + + // Also check RoleBindings (for custom roles or if system role check didn't match) + // This handles cases where subjects are bound to roles via RoleBindings + const allRules = [] + for (const subject of subjects) { + try { + const rules = await resolveRulesForSubject(subject, transaction) + if (rules && Array.isArray(rules)) { + allRules.push(...rules) + } + } catch (error) { + // Log error but continue to next subject + logger.error('Error resolving rules for subject:', JSON.stringify({ + error: error.message, + stack: error.stack, + subject + })) + } + } + + // Evaluate rules from RoleBindings + for (const rule of allRules) { + try { + if (evaluateRule(rule, apiGroup, resource, verb, resourceName)) { + const result = { allowed: true, reason: 'Rule matched' } + // Cache result + if (authCache.size < MAX_CACHE_SIZE) { + authCache.set(cacheKey, { result, timestamp: Date.now() }) + } + return result + } + } catch (ruleError) { + // Log error but continue to next rule + logger.error('Error evaluating rule from RoleBinding:', JSON.stringify({ + error: ruleError.message, + stack: ruleError.stack, + rule: rule, + apiGroup, + resource, + verb, + resourceName + })) + } + } + + // Deny by default + const result = { allowed: false, reason: 'Authorization denied: You do not have permission to perform this action. Please contact your administrator.' } + // Cache result + if (authCache.size < MAX_CACHE_SIZE) { + authCache.set(cacheKey, { result, timestamp: Date.now() }) + } + return result +} + +/** + * Clear authorization cache + */ +function clearCache () { + authCache.clear() +} + +/** + * Clean expired cache entries + */ +function cleanCache () { + const now = Date.now() + for (const [key, value] of authCache.entries()) { + if (now - value.timestamp >= CACHE_TTL) { + authCache.delete(key) + } + } +} + +// Clean cache every 10 minutes +setInterval(cleanCache, 10 * 60 * 1000) + +module.exports = { + authorize, + clearCache, + cleanCache +} + + diff --git a/src/lib/rbac/middleware.js b/src/lib/rbac/middleware.js new file mode 100644 index 00000000..017b176c --- /dev/null +++ b/src/lib/rbac/middleware.js @@ -0,0 +1,561 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const fs = require('fs') +const path = require('path') +const yaml = require('js-yaml') +const authorizer = require('./authorizer') +const logger = require('../../logger') +const config = require('../../config') + +// Load route resource catalog +let routeCatalog = null +function loadRouteCatalog () { + if (routeCatalog) { + return routeCatalog + } + + try { + const catalogPath = path.resolve(__dirname, '../../config/rbac-resources.yaml') + const fileContents = fs.readFileSync(catalogPath, 'utf8') + routeCatalog = yaml.load(fileContents) + return routeCatalog + } catch (error) { + logger.error('Failed to load RBAC route catalog:', error) + throw new Error('RBAC route catalog not found or invalid') + } +} + +/** + * Extract subjects from request (Keycloak token or ServiceAccount JWT) + */ +function extractSubjects (req) { + const subjects = [] + + // Extract from Keycloak token + if (req.kauth && req.kauth.grant && req.kauth.grant.access_token) { + const tokenContent = req.kauth.grant.access_token.content + + // Extract user + if (tokenContent.preferred_username) { + subjects.push({ + kind: 'User', + name: tokenContent.preferred_username + }) + } + + // // Extract groups from realm_access.roles or groups claim + // if (tokenContent.realm_access && tokenContent.realm_access.roles) { + // for (const role of tokenContent.realm_access.roles) { + // subjects.push({ + // kind: 'Group', + // name: role.toLowerCase() + // }) + // } + // } + + // Extract roles from resource_access[clientId].roles (client-specific roles) + const clientId = process.env.KC_CLIENT || config.get('auth.client.id') + if (clientId && tokenContent.resource_access && tokenContent.resource_access[clientId]) { + const clientRoles = tokenContent.resource_access[clientId].roles || [] + for (const role of clientRoles) { + subjects.push({ + kind: 'Group', + name: role.toLowerCase() + }) + } + } + + // // Extract groups from groups claim (if available) + // if (tokenContent.groups && Array.isArray(tokenContent.groups)) { + // for (const group of tokenContent.groups) { + // subjects.push({ + // kind: 'Group', + // name: group + // }) + // } + // } + } + + // // Extract from ServiceAccount JWT (if present in Authorization header) + // // This would be for agent-to-controller or microservice-to-controller communication + // if (req.headers.authorization) { + // const authHeader = req.headers.authorization + // if (authHeader.startsWith('Bearer ')) { + // // TODO: Parse JWT and extract ServiceAccount subject + // // For now, we'll handle this in a future update + // } + // } + + return subjects +} + +/** + * Find route definition in catalog + */ +function findRouteDefinition (method, path) { + const catalog = loadRouteCatalog() + if (!catalog || !catalog.resources) { + return null + } + + // Normalize path (remove trailing slashes and query parameters) + // Extract pathname only (remove query string and hash) + let normalizedPath = path + try { + // If path contains query parameters, extract just the pathname + if (path.includes('?')) { + normalizedPath = path.split('?')[0] + } + if (normalizedPath.includes('#')) { + normalizedPath = normalizedPath.split('#')[0] + } + } catch (error) { + // If parsing fails, use path as-is + } + normalizedPath = normalizedPath.replace(/\/$/, '') + + for (const [resourceName, resourceDef] of Object.entries(catalog.resources)) { + if (!resourceDef.routes || !Array.isArray(resourceDef.routes)) { + continue + } + + for (const route of resourceDef.routes) { + // Normalize route path (remove trailing slashes) before creating regex + const normalizedRoutePath = route.path.replace(/\/$/, '') + // Convert route path pattern to regex + // Replace :param with ([^/]+) for matching + const routePattern = normalizedRoutePath.replace(/:[^/]+/g, '([^/]+)') + const routeRegex = new RegExp(`^${routePattern}$`) + + if (routeRegex.test(normalizedPath)) { + const methods = route.methods || {} + // Support both HTTP methods and WebSocket (WS) + const methodKey = method.toUpperCase() === 'WS' ? 'WS' : method.toUpperCase() + const verbs = methods[methodKey] + + if (verbs && verbs.length > 0) { + // Extract resource name from path if resourceNameParam is specified + const matches = normalizedPath.match(routeRegex) + let resourceNameValue = null + if (route.resourceNameParam && matches) { + // Find the parameter index in the original path + const paramNames = [] + const paramPattern = /:([^/]+)/g + let match + while ((match = paramPattern.exec(route.path)) !== null) { + paramNames.push(match[1]) + } + const paramIndex = paramNames.indexOf(route.resourceNameParam) + if (paramIndex >= 0 && matches[paramIndex + 1]) { + resourceNameValue = matches[paramIndex + 1] + } + } + + return { + resource: resourceName, + verb: verbs[0], // Use first verb if multiple + resourceName: resourceNameValue, + apiGroup: '' // Core API group + } + } + } + } + } + + return null +} + +/** + * Extract subjects from WebSocket request (for WebSocket connections) + */ +function extractSubjectsFromWebSocket (req, token) { + const subjects = [] + + // For WebSocket, we need to verify the token and extract subjects + // This will be called from WebSocket handlers where token is already extracted + if (token) { + try { + // Parse JWT token to extract user info + const tokenParts = token.replace('Bearer ', '').split('.') + if (tokenParts.length === 3) { + const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString()) + + // Extract user + if (payload.preferred_username || payload.sub) { + subjects.push({ + kind: 'User', + name: payload.preferred_username || payload.sub + }) + } + + // // Extract groups from realm_access.roles + // // Commented out for security - only use client-scope roles + // if (payload.realm_access && payload.realm_access.roles) { + // for (const role of payload.realm_access.roles) { + // subjects.push({ + // kind: 'Group', + // name: role.toLowerCase() + // }) + // } + // } + + // Extract roles from resource_access[clientId].roles (client-specific roles) + // This is the secure approach - only client-scope roles + const clientId = process.env.KC_CLIENT || config.get('auth.client.id') + if (clientId && payload.resource_access && payload.resource_access[clientId]) { + const clientRoles = payload.resource_access[clientId].roles || [] + for (const role of clientRoles) { + subjects.push({ + kind: 'Group', + name: role.toLowerCase() + }) + } + } + + // // Extract groups from groups claim + // // Commented out - not used for RBAC authorization + // if (payload.groups && Array.isArray(payload.groups)) { + // for (const group of payload.groups) { + // subjects.push({ + // kind: 'Group', + // name: group + // }) + // } + // } + } + } catch (error) { + logger.warn('Failed to extract subjects from WebSocket token:', error) + } + } + + return subjects +} + +/** + * RBAC middleware factory for HTTP routes + * @param {string} resource - Resource name (optional, will be auto-detected from route) + * @param {string} verb - Verb (optional, will be auto-detected from route) + * @returns {Function} Express middleware + */ +function requirePermission (resource, verb) { + return async (req, res, next) => { + try { + // Extract subjects from request + const subjects = extractSubjects(req) + if (subjects.length === 0) { + logger.warn('No subjects found in request for RBAC authorization') + return res.status(401).json({ error: 'Unauthorized: No authentication information found' }) + } + + // Find route definition + const routeDef = findRouteDefinition(req.method, req.path) + if (!routeDef) { + // If route not in catalog, allow (for backward compatibility with non-RBAC routes) + logger.debug(`Route ${req.method} ${req.path} not found in RBAC catalog, allowing`) + return next() + } + + // Use provided resource/verb or route definition + const finalResource = resource || routeDef.resource + const finalVerb = verb || routeDef.verb + const resourceName = routeDef.resourceName + + // Get database transaction (create a fake transaction for read-only operations) + const transaction = { fakeTransaction: true } + + // Authorize + const authResult = await authorizer.authorize( + subjects, + routeDef.apiGroup || '', + finalResource, + finalVerb, + resourceName, + transaction + ) + + if (!authResult.allowed) { + logger.warn(`RBAC authorization denied: ${authResult.reason}`, { + subjects, + resource: finalResource, + verb: finalVerb, + resourceName, + path: req.path, + method: req.method + }) + return res.status(403).json({ + error: 'Forbidden', + message: authResult.reason || 'Access denied' + }) + } + + // Authorization successful + logger.debug('RBAC authorization allowed', { + subjects, + resource: finalResource, + verb: finalVerb, + resourceName + }) + + return next() + } catch (error) { + logger.error('RBAC middleware error:', error) + return res.status(500).json({ error: 'Internal server error during authorization' }) + } + } +} + +/** + * RBAC authorization check for WebSocket connections + * @param {Object} req - WebSocket request object + * @param {string} token - Bearer token from Authorization header or query params + * @returns {Promise<{allowed: boolean, reason?: string}>} + */ +async function authorizeWebSocket (req, token) { + let subjects = [] + try { + // Extract subjects from WebSocket token + subjects = extractSubjectsFromWebSocket(req, token) + if (subjects.length === 0) { + return { allowed: false, reason: 'No subjects found in WebSocket token' } + } + + // Find route definition (use 'WS' as method) + const routeDef = findRouteDefinition('WS', req.url) + if (!routeDef) { + // If route not in catalog, allow (for backward compatibility) + logger.debug(`WebSocket route ${req.url} not found in RBAC catalog, allowing`) + return { allowed: true, reason: 'Route not in catalog' } + } + + logger.debug(`WebSocket route found in catalog:`, { + url: req.url, + resource: routeDef.resource, + verb: routeDef.verb, + resourceName: routeDef.resourceName, + subjects: subjects + }) + + // Get database transaction + const transaction = { fakeTransaction: true } + + // Authorize + const authResult = await authorizer.authorize( + subjects, + routeDef.apiGroup || '', + routeDef.resource, + routeDef.verb, + routeDef.resourceName, + transaction + ) + + logger.debug(`WebSocket authorization result:`, { + allowed: authResult.allowed, + reason: authResult.reason, + resource: routeDef.resource, + verb: routeDef.verb, + subjects: subjects + }) + + if (!authResult.allowed) { + logger.warn(`RBAC authorization denied for WebSocket: ${authResult.reason}`, { + subjects, + resource: routeDef.resource, + verb: routeDef.verb, + resourceName: routeDef.resourceName, + path: req.url + }) + } + + return authResult + } catch (error) { + logger.error('RBAC WebSocket authorization error:', JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + subjects: subjects + })) + return { allowed: false, reason: 'Internal server error during authorization' } + } +} + +/** + * Middleware to skip RBAC (for public routes) + */ +function skipRBAC () { + return (req, res, next) => { + next() + } +} + +/** + * RBAC protection for WebSocket connections + * Handles token extraction from headers or query parameters + * @param {Function} handler - WebSocket handler function (ws, req) => {} + * @returns {Function} Protected WebSocket handler + */ +function protectWebSocket (handler) { + return async (ws, req) => { + try { + // Extract token from Authorization header first (for agent and CLI connections) + let token = req.headers.authorization + + // If no token in header, check query parameters (for React UI connections) + if (!token) { + logger.debug('Missing authentication token in header, checking query parameters') + try { + const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`) + const queryToken = url.searchParams.get('token') + if (queryToken) { + // Format as Bearer token and store in headers for consistency + token = `Bearer ${queryToken}` + req.headers.authorization = token + } + } catch (urlError) { + logger.warn('Failed to parse URL for token extraction:', urlError) + } + } + + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token neither in header nor query parameters') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:', error) + } + return + } + + // Create mock request object for route definition lookup + const mockReq = { + url: req.url, + method: 'WS' + } + + // Perform RBAC authorization + const authResult = await authorizeWebSocket(mockReq, token) + if (!authResult.allowed) { + logger.warn(`RBAC authorization denied for WebSocket: ${authResult.reason}`, { + path: req.url + }) + try { + ws.close(1008, authResult.reason || 'Access denied') + } catch (error) { + logger.error('Error closing WebSocket:', error) + } + return + } + + // Authorization successful - call the original handler + return handler(ws, req) + } catch (error) { + logger.error('WebSocket RBAC protection error:', error) + try { + if (ws.readyState === WebSocket.OPEN) { + ws.close(1008, error.message || 'Authorization failed') + } + } catch (closeError) { + logger.error('Error closing WebSocket:', closeError) + } + } + } +} + +/** + * RBAC protect function - drop-in replacement for keycloak.protect() + * + * NOTE: The roles parameter is IGNORED. Authorization is determined automatically + * from the route catalog (rbac-resources.yaml) and RoleBindings in the database. + * + * Do NOT pass static role arrays. All authorization is based on fine-grained RBAC rules + * defined via Role and RoleBinding objects. + * + * @param {string|Array} _roles - IGNORED (kept for backward compatibility only) + * @returns {Function} Middleware function compatible with keycloak.protect() pattern + */ +function protect (_roles) { + // _roles parameter is ignored - authorization determined from route catalog and RoleBindings + // Return a function that matches keycloak.protect() signature: (req, res, callback) => {} + return async (req, res, callback) => { + try { + // Extract subjects from request + const subjects = extractSubjects(req) + if (subjects.length === 0) { + logger.warn('No subjects found in request for RBAC authorization') + return res.status(401).json({ error: 'Unauthorized: No authentication information found' }) + } + + // Find route definition + const routeDef = findRouteDefinition(req.method, req.path) + if (!routeDef) { + // If route not in catalog, allow (for backward compatibility with non-RBAC routes) + logger.debug(`Route ${req.method} ${req.path} not found in RBAC catalog, allowing`) + return callback() + } + + // Get database transaction + const transaction = { fakeTransaction: true } + + // Authorize + const authResult = await authorizer.authorize( + subjects, + routeDef.apiGroup || '', + routeDef.resource, + routeDef.verb, + routeDef.resourceName, + transaction + ) + + if (!authResult.allowed) { + logger.warn(`RBAC authorization denied: ${authResult.reason}`, { + subjects, + resource: routeDef.resource, + verb: routeDef.verb, + resourceName: routeDef.resourceName, + path: req.path, + method: req.method + }) + return res.status(403).json({ + error: 'Forbidden', + message: authResult.reason || 'Access denied' + }) + } + + // Authorization successful - call the callback + logger.debug('RBAC authorization allowed', { + subjects, + resource: routeDef.resource, + verb: routeDef.verb, + resourceName: routeDef.resourceName + }) + + return callback() + } catch (error) { + logger.error('RBAC middleware error:', error) + return res.status(500).json({ error: 'Internal server error during authorization' }) + } + } +} + +module.exports = { + requirePermission, + protect, // Drop-in replacement for keycloak.protect() + authorizeWebSocket, + protectWebSocket, // RBAC protection for WebSocket connections + skipRBAC, + extractSubjects, + extractSubjectsFromWebSocket, + findRouteDefinition +} + + diff --git a/src/routes/agent.js b/src/routes/agent.js index 980098d4..986cf3c0 100644 --- a/src/routes/agent.js +++ b/src/routes/agent.js @@ -17,6 +17,7 @@ const ResponseDecorator = require('../decorators/response-decorator') const WebSocketServer = require('../websocket/server') const Errors = require('../helpers/errors') const logger = require('../logger') +const TransactionDecorator = require('../decorators/transaction-decorator') module.exports = [ { @@ -712,9 +713,16 @@ module.exports = [ return } - // Initialize WebSocket connection for agent + // Set flag to bypass route matching + // Token validation will be done by validateAgentConnection in handleAgentConnection + req._rbacAuthorized = true + + // Call handler directly (it will validate the token) const wsServer = WebSocketServer.getInstance() - await wsServer.handleConnection(ws, req) + const microserviceUuid = req.params.microserviceUuid + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleAgentConnection(ws, req, token, microserviceUuid, transaction) + })() } catch (error) { logger.error('Error in agent WebSocket connection:' + JSON.stringify({ error: error.message, @@ -755,9 +763,17 @@ module.exports = [ return } - // Initialize WebSocket connection for agent microservice logs + // Set flag to bypass route matching + // Token validation will be done by validateAgentLogsConnection in handleAgentLogsConnection + req._rbacAuthorized = true + + // Call handler directly (it will validate the token) const wsServer = WebSocketServer.getInstance() - await wsServer.handleConnection(ws, req) + const microserviceUuid = req.params.microserviceUuid + const sessionId = req.params.sessionId + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleAgentLogsConnection(ws, req, token, microserviceUuid, null, sessionId, transaction) + })() } catch (error) { logger.error('Error in agent microservice logs WebSocket connection:' + JSON.stringify({ error: error.message, @@ -799,9 +815,17 @@ module.exports = [ return } - // Initialize WebSocket connection for agent fog logs + // Set flag to bypass route matching + // Token validation will be done by validateAgentLogsConnection in handleAgentLogsConnection + req._rbacAuthorized = true + + // Call handler directly (it will validate the token) const wsServer = WebSocketServer.getInstance() - await wsServer.handleConnection(ws, req) + const iofogUuid = req.params.iofogUuid + const sessionId = req.params.sessionId + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleAgentLogsConnection(ws, req, token, null, iofogUuid, sessionId, transaction) + })() } catch (error) { logger.error('Error in agent fog logs WebSocket connection:' + JSON.stringify({ error: error.message, diff --git a/src/routes/application.js b/src/routes/application.js index 3feb7c94..3f403182 100644 --- a/src/routes/application.js +++ b/src/routes/application.js @@ -15,7 +15,7 @@ const ApplicationController = require('../controllers/application-controller') const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -34,10 +34,10 @@ module.exports = [ const getApplicationsByUserEndPoint = ResponseDecorator.handleErrors(ApplicationController.getApplicationsByUserEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await getApplicationsByUserEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -62,10 +62,10 @@ module.exports = [ const getApplicationsBySystemEndPoint = ResponseDecorator.handleErrors(ApplicationController.getApplicationsBySystemEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await getApplicationsBySystemEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -95,10 +95,10 @@ module.exports = [ const createApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.createApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await createApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -128,10 +128,10 @@ module.exports = [ const createApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.createApplicationYAMLEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await createApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -160,10 +160,10 @@ module.exports = [ const getApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.getApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await getApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -193,10 +193,10 @@ module.exports = [ const getSystemApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.getSystemApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await getSystemApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -231,10 +231,10 @@ module.exports = [ const updateApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.patchApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await updateApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -268,10 +268,10 @@ module.exports = [ const updateApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.updateApplicationYAMLEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await updateApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -305,10 +305,10 @@ module.exports = [ const updateApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.updateApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await updateApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -337,10 +337,10 @@ module.exports = [ const deleteApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.deleteApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await deleteApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -369,8 +369,8 @@ module.exports = [ const deleteSystemApplicationEndPoint = ResponseDecorator.handleErrors(ApplicationController.deleteSystemApplicationEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await deleteSystemApplicationEndPoint(req) const user = req.kauth.grant.access_token.content.preferred_username res diff --git a/src/routes/applicationTemplate.js b/src/routes/applicationTemplate.js index f0bae7b2..1f46be64 100644 --- a/src/routes/applicationTemplate.js +++ b/src/routes/applicationTemplate.js @@ -15,7 +15,7 @@ const ApplicationTemplateController = require('../controllers/application-templa const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -34,10 +34,10 @@ module.exports = [ const getApplicationTemplatesByUserEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.getApplicationTemplatesByUserEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await getApplicationTemplatesByUserEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -66,10 +66,10 @@ module.exports = [ const createApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.createApplicationTemplateEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await createApplicationTemplateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -99,10 +99,10 @@ module.exports = [ const createApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.createApplicationTemplateYAMLEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await createApplicationTemplateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -131,10 +131,10 @@ module.exports = [ const getApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.getApplicationTemplateEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await getApplicationTemplateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -167,10 +167,10 @@ module.exports = [ const patchApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.patchApplicationTemplateEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await patchApplicationTemplateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -204,10 +204,10 @@ module.exports = [ const updateApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.updateApplicationTemplateYAMLEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await updateApplicationTemplateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -240,10 +240,10 @@ module.exports = [ const updateApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.updateApplicationTemplateEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await updateApplicationTemplateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -272,8 +272,8 @@ module.exports = [ const deleteApplicationTemplateEndPoint = ResponseDecorator.handleErrors(ApplicationTemplateController.deleteApplicationTemplateEndPoint, successCode, errorCodes) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await deleteApplicationTemplateEndPoint(req) const user = req.kauth.grant.access_token.content.preferred_username res diff --git a/src/routes/capabilities.js b/src/routes/capabilities.js index f8b533e7..5c20ea3b 100644 --- a/src/routes/capabilities.js +++ b/src/routes/capabilities.js @@ -11,7 +11,7 @@ * */ const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -20,8 +20,8 @@ module.exports = [ middleware: async (req, res) => { logger.apiReq(req) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { res.sendStatus(204) }) } @@ -32,8 +32,8 @@ module.exports = [ middleware: async (req, res) => { logger.apiReq(req) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { res.sendStatus(204) }) } diff --git a/src/routes/catalog.js b/src/routes/catalog.js index 66580b17..e794da14 100644 --- a/src/routes/catalog.js +++ b/src/routes/catalog.js @@ -15,7 +15,7 @@ const CatalogController = require('../controllers/catalog-controller') const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -38,10 +38,10 @@ module.exports = [ errorCodes ) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await listCatalogItemsEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -79,10 +79,10 @@ module.exports = [ errorCodes ) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await createCatalogItemEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -115,10 +115,10 @@ module.exports = [ errorCodes ) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await listCatalogItemEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -160,10 +160,10 @@ module.exports = [ errorCodes ) - // Add keycloak.protect() middleware to protect the route for SRE and Developer - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE and Developer + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await updateCatalogItemEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -196,8 +196,8 @@ module.exports = [ errorCodes ) - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const responseObject = await deleteCatalogItemEndPoint(req) const user = req.kauth.grant.access_token.content.preferred_username res diff --git a/src/routes/certificate.js b/src/routes/certificate.js index 2f30cfbc..b79de0b6 100644 --- a/src/routes/certificate.js +++ b/src/routes/certificate.js @@ -3,7 +3,7 @@ const CertificateController = require('../controllers/certificate-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -28,10 +28,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createCAEndpoint = ResponseDecorator.handleErrors(CertificateController.createCAEndpoint, successCode, errorCodes) const responseObject = await createCAEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -58,10 +58,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getCAEndpoint = ResponseDecorator.handleErrors(CertificateController.getCAEndpoint, successCode, errorCodes) const responseObject = await getCAEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -84,10 +84,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listCAEndpoint = ResponseDecorator.handleErrors(CertificateController.listCAEndpoint, successCode, errorCodes) const responseObject = await listCAEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -114,10 +114,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteCAEndpoint = ResponseDecorator.handleErrors(CertificateController.deleteCAEndpoint, successCode, errorCodes) const responseObject = await deleteCAEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -152,10 +152,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createCertificateEndpoint = ResponseDecorator.handleErrors(CertificateController.createCertificateEndpoint, successCode, errorCodes) const responseObject = await createCertificateEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -182,10 +182,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listExpiringCertificatesEndpoint = ResponseDecorator.handleErrors(CertificateController.listExpiringCertificatesEndpoint, successCode, errorCodes) const responseObject = await listExpiringCertificatesEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -212,10 +212,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getCertificateEndpoint = ResponseDecorator.handleErrors(CertificateController.getCertificateEndpoint, successCode, errorCodes) const responseObject = await getCertificateEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -238,10 +238,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listCertificatesEndpoint = ResponseDecorator.handleErrors(CertificateController.listCertificatesEndpoint, successCode, errorCodes) const responseObject = await listCertificatesEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -268,10 +268,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteCertificateEndpoint = ResponseDecorator.handleErrors(CertificateController.deleteCertificateEndpoint, successCode, errorCodes) const responseObject = await deleteCertificateEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -302,10 +302,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const renewCertificateEndpoint = ResponseDecorator.handleErrors(CertificateController.renewCertificateEndpoint, successCode, errorCodes) const responseObject = await renewCertificateEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -341,7 +341,7 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createCertificateFromYamlEndpoint = ResponseDecorator.handleErrors(CertificateController.createCertificateFromYamlEndpoint, successCode, errorCodes) const responseObject = await createCertificateFromYamlEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/cluster.js b/src/routes/cluster.js new file mode 100644 index 00000000..c49595ab --- /dev/null +++ b/src/routes/cluster.js @@ -0,0 +1,145 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ +const constants = require('../helpers/constants') +const ClusterController = require('../controllers/cluster-controller') +const ResponseDecorator = require('../decorators/response-decorator') +const Errors = require('../helpers/errors') +const logger = require('../logger') +const rbacMiddleware = require('../lib/rbac/middleware') + +module.exports = [ + { + method: 'get', + path: '/api/v3/cluster/controllers', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + const listClusterControllersEndPoint = ResponseDecorator.handleErrors(ClusterController.listClusterControllersEndPoint, successCode, errorCodes) + + await rbacMiddleware.protect()(req, res, async () => { + const responseObject = await listClusterControllersEndPoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'get', + path: '/api/v3/cluster/controllers/:uuid', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + const getClusterControllerEndPoint = ResponseDecorator.handleErrors(ClusterController.getClusterControllerEndPoint, successCode, errorCodes) + + await rbacMiddleware.protect()(req, res, async () => { + const responseObject = await getClusterControllerEndPoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/cluster/controllers/:uuid', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + } + ] + + const updateClusterControllerEndPoint = ResponseDecorator.handleErrors(ClusterController.updateClusterControllerEndPoint, successCode, errorCodes) + + await rbacMiddleware.protect()(req, res, async () => { + const responseObject = await updateClusterControllerEndPoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'delete', + path: '/api/v3/cluster/controllers/:uuid', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + const deleteClusterControllerEndPoint = ResponseDecorator.handleErrors(ClusterController.deleteClusterControllerEndPoint, successCode, errorCodes) + + await rbacMiddleware.protect()(req, res, async () => { + const responseObject = await deleteClusterControllerEndPoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + } +] diff --git a/src/routes/config.js b/src/routes/config.js index 60d6b347..edae06ba 100644 --- a/src/routes/config.js +++ b/src/routes/config.js @@ -15,7 +15,7 @@ const ConfigController = require('../controllers/config-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -32,11 +32,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const getConfigEndpoint = ResponseDecorator.handleErrors(ConfigController.listConfigEndpoint, successCode, errorCodes) const responseObject = await getConfigEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -63,11 +63,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const getConfigEndpoint = ResponseDecorator.handleErrors(ConfigController.getConfigEndpoint, successCode, errorCodes) const responseObject = await getConfigEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -95,8 +95,8 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const upsertConfigElementEndpoint = ResponseDecorator.handleErrors(ConfigController.upsertConfigElementEndpoint, successCode, errorCodes) const responseObject = await upsertConfigElementEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/configMap.js b/src/routes/configMap.js index 4d648419..3e96e2d3 100644 --- a/src/routes/configMap.js +++ b/src/routes/configMap.js @@ -16,7 +16,7 @@ const ConfigMapController = require('../controllers/config-map-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -41,10 +41,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createConfigMapEndpoint = ResponseDecorator.handleErrors(ConfigMapController.createConfigMapEndpoint, successCode, errorCodes) const responseObject = await createConfigMapEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -76,10 +76,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createConfigMapFromYamlEndpoint = ResponseDecorator.handleErrors(ConfigMapController.createConfigMapFromYamlEndpoint, successCode, errorCodes) const responseObject = await createConfigMapFromYamlEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -110,10 +110,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateConfigMapEndpoint = ResponseDecorator.handleErrors(ConfigMapController.updateConfigMapEndpoint, successCode, errorCodes) const responseObject = await updateConfigMapEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -145,10 +145,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateConfigMapFromYamlEndpoint = ResponseDecorator.handleErrors(ConfigMapController.updateConfigMapFromYamlEndpoint, successCode, errorCodes) const responseObject = await updateConfigMapFromYamlEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -175,10 +175,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getConfigMapEndpoint = ResponseDecorator.handleErrors(ConfigMapController.getConfigMapEndpoint, successCode, errorCodes) const responseObject = await getConfigMapEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -201,10 +201,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listConfigMapsEndpoint = ResponseDecorator.handleErrors(ConfigMapController.listConfigMapsEndpoint, successCode, errorCodes) const responseObject = await listConfigMapsEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -231,7 +231,7 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteConfigMapEndpoint = ResponseDecorator.handleErrors(ConfigMapController.deleteConfigMapEndpoint, successCode, errorCodes) const responseObject = await deleteConfigMapEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/diagnostics.js b/src/routes/diagnostics.js index 49e83a33..cf2d4dd8 100644 --- a/src/routes/diagnostics.js +++ b/src/routes/diagnostics.js @@ -16,7 +16,7 @@ const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const fs = require('fs') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -37,15 +37,15 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const createMicroserviceImageSnapshotEndPoint = ResponseDecorator.handleErrors( DiagnosticController.createMicroserviceImageSnapshotEndPoint, successCode, errorCodes ) const responseObject = await createMicroserviceImageSnapshotEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -72,15 +72,15 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const getMicroserviceImageSnapshotEndPoint = ResponseDecorator.handleErrors( DiagnosticController.getMicroserviceImageSnapshotEndPoint, successCode, errorCodes ) const responseObject = await getMicroserviceImageSnapshotEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' if (responseObject.code !== successCode) { res .status(responseObject.code) @@ -120,15 +120,15 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const changeMicroserviceStraceStateEndPoint = ResponseDecorator.handleErrors( DiagnosticController.changeMicroserviceStraceStateEndPoint, successCode, errorCodes ) const responseObject = await changeMicroserviceStraceStateEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -155,15 +155,15 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const getMicroserviceStraceDataEndPoint = ResponseDecorator.handleErrors( DiagnosticController.getMicroserviceStraceDataEndPoint, successCode, errorCodes ) const responseObject = await getMicroserviceStraceDataEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -198,8 +198,8 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route + await rbacMiddleware.protect()(req, res, async () => { const postMicroserviceStraceDataToFtpEndPoint = ResponseDecorator.handleErrors( DiagnosticController.postMicroserviceStraceDataToFtpEndPoint, successCode, diff --git a/src/routes/edgeResource.js b/src/routes/edgeResource.js index 0f55c3e3..54208811 100644 --- a/src/routes/edgeResource.js +++ b/src/routes/edgeResource.js @@ -15,7 +15,7 @@ const EdgeResourceController = require('../controllers/edge-resource-controller' const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -32,11 +32,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const getEdgeResourcesEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.listEdgeResourcesEndpoint, successCode, errorCodes) const responseObject = await getEdgeResourcesEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -63,11 +63,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const getEdgeResourceEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.getEdgeResourceEndpoint, successCode, errorCodes) const responseObject = await getEdgeResourceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -94,11 +94,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const getEdgeResourceAllVersionsEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.getEdgeResourceAllVersionsEndpoint, successCode, errorCodes) const responseObject = await getEdgeResourceAllVersionsEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -130,11 +130,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const updateEdgeResourceEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.updateEdgeResourceEndpoint, successCode, errorCodes) const responseObject = await updateEdgeResourceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -165,11 +165,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const deleteEdgeResourceEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.deleteEdgeResourceEndpoint, successCode, errorCodes) const responseObject = await deleteEdgeResourceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -197,11 +197,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const createEdgeResourceEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.createEdgeResourceEndpoint, successCode, errorCodes) const responseObject = await createEdgeResourceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -228,11 +228,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const linkEdgeResourceEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.linkEdgeResourceEndpoint, successCode, errorCodes) const responseObject = await linkEdgeResourceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -259,8 +259,8 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const unlinkEdgeResourceEndpoint = ResponseDecorator.handleErrors(EdgeResourceController.unlinkEdgeResourceEndpoint, successCode, errorCodes) const responseObject = await unlinkEdgeResourceEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/event.js b/src/routes/event.js index d9588674..e9b5e082 100644 --- a/src/routes/event.js +++ b/src/routes/event.js @@ -16,7 +16,7 @@ const EventController = require('../controllers/event-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -38,10 +38,10 @@ module.exports = [ ] // Protected with Keycloak (SRE role only) - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listEventsEndpoint = ResponseDecorator.handleErrors(EventController.listEventsEndpoint, successCode, errorCodes) const responseObject = await listEventsEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -73,7 +73,7 @@ module.exports = [ ] // Protected with Keycloak (SRE role only) - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteOldEventsEndpoint = ResponseDecorator.handleErrors(EventController.deleteOldEventsEndpoint, successCode, errorCodes) const responseObject = await deleteOldEventsEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/flow.js b/src/routes/flow.js index e9397c04..3de28156 100644 --- a/src/routes/flow.js +++ b/src/routes/flow.js @@ -15,7 +15,7 @@ const FlowController = require('../controllers/application-controller') const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -32,11 +32,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for both SRE and Developer roles - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for both SRE and Developer roles + await rbacMiddleware.protect()(req, res, async () => { const getFlowsByUserEndPoint = ResponseDecorator.handleErrors(FlowController.getApplicationsByUserEndPoint, successCode, errorCodes) const responseObject = await getFlowsByUserEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send({ flows: responseObject.body.applications }) @@ -63,11 +63,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for both SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for both SRE and Developer roles + await rbacMiddleware.protect()(req, res, async () => { const createFlowEndPoint = ResponseDecorator.handleErrors(FlowController.createApplicationEndPoint, successCode, errorCodes) const responseObject = await createFlowEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -94,11 +94,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for both SRE and Developer roles - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for both SRE and Developer roles + await rbacMiddleware.protect()(req, res, async () => { const getFlowEndPoint = ResponseDecorator.handleErrors(FlowController.getApplicationByIdEndPoint, successCode, errorCodes) const responseObject = await getFlowEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -129,11 +129,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for both SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for both SRE and Developer roles + await rbacMiddleware.protect()(req, res, async () => { const updateFlowEndPoint = ResponseDecorator.handleErrors(FlowController.patchApplicationByIdEndPoint, successCode, errorCodes) const responseObject = await updateFlowEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -160,8 +160,8 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for both SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for both SRE and Developer roles + await rbacMiddleware.protect()(req, res, async () => { const deleteFlowEndPoint = ResponseDecorator.handleErrors(FlowController.deleteApplicationByIdEndPoint, successCode, errorCodes) const responseObject = await deleteFlowEndPoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/iofog.js b/src/routes/iofog.js index fe46d1d0..12801059 100644 --- a/src/routes/iofog.js +++ b/src/routes/iofog.js @@ -15,7 +15,7 @@ const FogController = require('../controllers/iofog-controller') const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -36,11 +36,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE, Developer, and Viewer roles - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE, Developer, and Viewer roles + await rbacMiddleware.protect()(req, res, async () => { const getFogList = ResponseDecorator.handleErrors(FogController.getFogListEndPoint, successCode, errCodes) const responseObject = await getFogList(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -69,10 +69,10 @@ module.exports = [ ] // Protect the route with SRE access control - await keycloak.protect('SRE')(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createFog = ResponseDecorator.handleErrors(FogController.createFogEndPoint, successCode, errCodes) const responseObject = await createFog(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -105,10 +105,10 @@ module.exports = [ ] // Protect the route with SRE access control - await keycloak.protect('SRE')(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateFog = ResponseDecorator.handleErrors(FogController.updateFogEndPoint, successCode, errCodes) const responseObject = await updateFog(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -136,10 +136,10 @@ module.exports = [ ] // Protect the route with SRE access control - await keycloak.protect('SRE')(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteFog = ResponseDecorator.handleErrors(FogController.deleteFogEndPoint, successCode, errCodes) const responseObject = await deleteFog(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -167,10 +167,10 @@ module.exports = [ ] // Protect the route with SRE, Developer, and Viewer access control - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getFog = ResponseDecorator.handleErrors(FogController.getFogEndPoint, successCode, errCodes) const responseObject = await getFog(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -197,11 +197,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const generateFogProvisioningKey = ResponseDecorator.handleErrors(FogController.generateProvisioningKeyEndPoint, successCode, errCodes) const responseObject = await generateFogProvisioningKey(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -232,11 +232,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const setFogVersionCommand = ResponseDecorator.handleErrors(FogController.setFogVersionCommandEndPoint, successCode, errCodes) const responseObject = await setFogVersionCommand(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -267,11 +267,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const setFogRebootCommand = ResponseDecorator.handleErrors(FogController.setFogRebootCommandEndPoint, successCode, errCodes) const responseObject = await setFogRebootCommand(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -298,11 +298,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getHalHardwareInfo = ResponseDecorator.handleErrors(FogController.getHalHardwareInfoEndPoint, successCode, errCodes) const responseObject = await getHalHardwareInfo(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -329,10 +329,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getHalUsbInfo = ResponseDecorator.handleErrors(FogController.getHalUsbInfoEndPoint, successCode, errCodes) const responseObject = await getHalUsbInfo(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -363,11 +363,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const setFogPruneCommand = ResponseDecorator.handleErrors(FogController.setFogPruneCommandEndPoint, successCode, errCodes) const responseObject = await setFogPruneCommand(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -398,11 +398,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const enableNodeExecEndPoint = ResponseDecorator.handleErrors(FogController.enableNodeExecEndPoint, successCode, errCodes) const responseObject = await enableNodeExecEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -433,7 +433,7 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const disableNodeExecEndPoint = ResponseDecorator.handleErrors(FogController.disableNodeExecEndPoint, successCode, errCodes) const responseObject = await disableNodeExecEndPoint(req) @@ -449,27 +449,35 @@ module.exports = [ { method: 'ws', path: '/api/v3/iofog/:uuid/logs', - middleware: async (ws, req) => { + middleware: rbacMiddleware.protectWebSocket(async (ws, req) => { const WebSocketServer = require('../websocket/server') + const TransactionDecorator = require('../decorators/transaction-decorator') logger.apiReq(req) try { + // RBAC already passed, token already extracted by protectWebSocket + // Set flag to indicate RBAC authorization is complete + req._rbacAuthorized = true + + // Extract token from headers (set by protectWebSocket) const token = req.headers.authorization if (!token) { logger.error('WebSocket connection failed: Missing authentication token') try { ws.close(1008, 'Missing authentication token') } catch (error) { - logger.error('Error closing WebSocket:' + JSON.stringify({ - error: error.message, - originalError: 'Missing authentication token' - })) + logger.error('Error closing WebSocket:', error.message) } return } - // Initialize WebSocket connection for fog logs + // Call handler directly with extracted parameters const wsServer = WebSocketServer.getInstance() - await wsServer.handleConnection(ws, req) + const microserviceUuid = null + const fogUuid = req.params.uuid + const expectSystem = false + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, expectSystem, transaction) + })() } catch (error) { logger.error('Error in fog logs WebSocket connection:' + JSON.stringify({ error: error.message, @@ -479,7 +487,7 @@ module.exports = [ })) try { if (ws.readyState === ws.OPEN) { - ws.close(1008, error.message || 'Authentication failed') + ws.close(1008, error.message || 'Connection failed') } } catch (closeError) { logger.error('Error closing fog logs WebSocket:' + JSON.stringify({ @@ -488,6 +496,6 @@ module.exports = [ })) } } - } + }) } ] diff --git a/src/routes/microservices.js b/src/routes/microservices.js index 6ecad2d2..e3f68e01 100644 --- a/src/routes/microservices.js +++ b/src/routes/microservices.js @@ -15,8 +15,9 @@ const MicroservicesController = require('../controllers/microservices-controller const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') const WebSocketServer = require('../websocket/server') +const TransactionDecorator = require('../decorators/transaction-decorator') module.exports = [ { @@ -33,11 +34,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getMicroservicesByApplicationEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getMicroservicesByApplicationEndPoint, successCode, errorCodes) const responseObject = await getMicroservicesByApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -60,11 +61,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getSystemMicroservicesByApplicationEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getSystemMicroservicesByApplicationEndPoint, successCode, errorCodes) const responseObject = await getSystemMicroservicesByApplicationEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -92,11 +93,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createMicroservicesOnFogEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createMicroserviceOnFogEndPoint, successCode, errorCodes) const responseObject = await createMicroservicesOnFogEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -125,11 +126,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createMicroservicesYAMLEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createMicroserviceYAMLEndPoint, successCode, errorCodes) const responseObject = await createMicroservicesYAMLEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -156,11 +157,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getMicroserviceEndPoint, successCode, errorCodes) const responseObject = await getMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -187,11 +188,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getSystemMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getSystemMicroserviceEndPoint, successCode, errorCodes) const responseObject = await getSystemMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -218,11 +219,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listMicroserviceByPubTagEndPoint = ResponseDecorator.handleErrors(MicroservicesController.listMicroserviceByPubTagEndPoint, successCode, errorCodes) const responseObject = await listMicroserviceByPubTagEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -249,11 +250,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listMicroserviceBySubTagEndPoint = ResponseDecorator.handleErrors(MicroservicesController.listMicroserviceBySubTagEndPoint, successCode, errorCodes) const responseObject = await listMicroserviceBySubTagEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -285,11 +286,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateMicroserviceEndPoint, successCode, errorCodes) const responseObject = await updateMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -321,11 +322,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateSystemMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateSystemMicroserviceEndPoint, successCode, errorCodes) const responseObject = await updateSystemMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -356,11 +357,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const rebuildMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.rebuildMicroserviceEndPoint, successCode, errorCodes) const responseObject = await rebuildMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -391,11 +392,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const rebuildSystemMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.rebuildSystemMicroserviceEndPoint, successCode, errorCodes) const responseObject = await rebuildSystemMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -428,11 +429,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateMicroserviceYAMLEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateMicroserviceYAMLEndPoint, successCode, errorCodes) const responseObject = await updateMicroserviceYAMLEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -465,11 +466,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateSystemMicroserviceYAMLEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateSystemMicroserviceYAMLEndPoint, successCode, errorCodes) const responseObject = await updateSystemMicroserviceYAMLEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -500,11 +501,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getMicroserviceConfigEndPoint, successCode, errorCodes) const responseObject = await getMicroserviceConfigEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -535,11 +536,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateMicroserviceConfigEndPoint, successCode, errorCodes) const responseObject = await updateMicroserviceConfigEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -570,11 +571,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getSystemMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getSystemMicroserviceConfigEndPoint, successCode, errorCodes) const responseObject = await getSystemMicroserviceConfigEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -605,11 +606,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateSystemMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateSystemMicroserviceConfigEndPoint, successCode, errorCodes) const responseObject = await updateSystemMicroserviceConfigEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -640,11 +641,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.deleteMicroserviceConfigEndPoint, successCode, errorCodes) const responseObject = await deleteMicroserviceConfigEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -675,11 +676,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteSystemMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.deleteSystemMicroserviceConfigEndPoint, successCode, errorCodes) const responseObject = await deleteSystemMicroserviceConfigEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -706,11 +707,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.deleteMicroserviceEndPoint, successCode, errorCodes) const responseObject = await deleteMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -741,11 +742,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createMicroserviceRouteEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createMicroserviceRouteEndPoint, successCode, errorCodes) const responseObject = await createMicroserviceRouteEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -776,11 +777,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteMicroserviceRouteEndPoint = ResponseDecorator.handleErrors( MicroservicesController.deleteMicroserviceRouteEndPoint, successCode, errorCodes) const responseObject = await deleteMicroserviceRouteEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -811,11 +812,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createMicroservicePortMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createMicroservicePortMappingEndPoint, successCode, errorCodes) const responseObject = await createMicroservicePortMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -846,11 +847,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createSystemMicroservicePortMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createSystemMicroservicePortMappingEndPoint, successCode, errorCodes) const responseObject = await createSystemMicroservicePortMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -877,11 +878,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteMicroservicePortMapping = ResponseDecorator.handleErrors( MicroservicesController.deleteMicroservicePortMappingEndPoint, successCode, errorCodes) const responseObject = await deleteMicroservicePortMapping(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -908,11 +909,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteSystemMicroservicePortMapping = ResponseDecorator.handleErrors( MicroservicesController.deleteSystemMicroservicePortMappingEndPoint, successCode, errorCodes) const responseObject = await deleteSystemMicroservicePortMapping(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -939,11 +940,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getMicroservicePortMapping = ResponseDecorator.handleErrors( MicroservicesController.getMicroservicePortMappingListEndPoint, successCode, errorCodes) const responseObject = await getMicroservicePortMapping(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -970,14 +971,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listMicroserviceVolumeMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.listMicroserviceVolumeMappingsEndPoint, successCode, errorCodes ) const responseObject = await listMicroserviceVolumeMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1008,14 +1009,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createMicroserviceVolumeMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createMicroserviceVolumeMappingEndPoint, successCode, errorCodes ) const responseObject = await createMicroserviceVolumeMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1046,14 +1047,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createSystemMicroserviceVolumeMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createSystemMicroserviceVolumeMappingEndPoint, successCode, errorCodes ) const responseObject = await createSystemMicroserviceVolumeMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1084,14 +1085,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteMicroserviceVolumeMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.deleteMicroserviceVolumeMappingEndPoint, successCode, errorCodes ) const responseObject = await deleteMicroserviceVolumeMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1122,14 +1123,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteSystemMicroserviceVolumeMappingEndPoint = ResponseDecorator.handleErrors( MicroservicesController.deleteSystemMicroserviceVolumeMappingEndPoint, successCode, errorCodes ) const responseObject = await deleteSystemMicroserviceVolumeMappingEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1160,14 +1161,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createMicroserviceExecEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createMicroserviceExecEndPoint, successCode, errorCodes ) const responseObject = await createMicroserviceExecEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1198,14 +1199,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createSystemMicroserviceExecEndPoint = ResponseDecorator.handleErrors( MicroservicesController.createSystemMicroserviceExecEndPoint, successCode, errorCodes ) const responseObject = await createSystemMicroserviceExecEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1236,14 +1237,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteMicroserviceExecEndPoint = ResponseDecorator.handleErrors( MicroservicesController.deleteMicroserviceExecEndPoint, successCode, errorCodes ) const responseObject = await deleteMicroserviceExecEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1274,14 +1275,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteSystemMicroserviceExecEndPoint = ResponseDecorator.handleErrors( MicroservicesController.deleteSystemMicroserviceExecEndPoint, successCode, errorCodes ) const responseObject = await deleteSystemMicroserviceExecEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1312,11 +1313,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const startMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.startMicroserviceEndPoint, successCode, errorCodes) const responseObject = await startMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1347,11 +1348,11 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const stopMicroserviceEndPoint = ResponseDecorator.handleErrors(MicroservicesController.stopMicroserviceEndPoint, successCode, errorCodes) const responseObject = await stopMicroserviceEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -1363,26 +1364,31 @@ module.exports = [ { method: 'ws', path: '/api/v3/microservices/exec/:microserviceUuid', - middleware: async (ws, req) => { + middleware: rbacMiddleware.protectWebSocket(async (ws, req) => { logger.apiReq(req) try { + // RBAC already passed, token already extracted by protectWebSocket + // Set flag to indicate RBAC authorization is complete + req._rbacAuthorized = true + + // Extract token from headers (set by protectWebSocket) const token = req.headers.authorization if (!token) { logger.error('WebSocket connection failed: Missing authentication token') try { ws.close(1008, 'Missing authentication token') } catch (error) { - logger.error('Error closing WebSocket:' + JSON.stringify({ - error: error.message, - originalError: 'Missing authentication token' - })) + logger.error('Error closing WebSocket:', error.message) } return } - // Initialize WebSocket connection for microservice + // Call handler directly const wsServer = WebSocketServer.getInstance() - await wsServer.handleConnection(ws, req) + const microserviceUuid = req.params.microserviceUuid + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleUserConnection(ws, req, token, microserviceUuid, false, transaction) + })() } catch (error) { logger.error('Error in microservice WebSocket connection:' + JSON.stringify({ error: error.message, @@ -1392,7 +1398,7 @@ module.exports = [ })) try { if (ws.readyState === ws.OPEN) { - ws.close(1008, error.message || 'Authentication failed') + ws.close(1008, error.message || 'Connection failed') } } catch (closeError) { logger.error('Error closing microservice WebSocket:' + JSON.stringify({ @@ -1401,31 +1407,86 @@ module.exports = [ })) } } - } + }) + }, + { + method: 'ws', + path: '/api/v3/microservices/system/exec/:microserviceUuid', + middleware: rbacMiddleware.protectWebSocket(async (ws, req) => { + logger.apiReq(req) + try { + // RBAC already passed, token already extracted by protectWebSocket + // Set flag to indicate RBAC authorization is complete + req._rbacAuthorized = true + + // Extract token from headers (set by protectWebSocket) + const token = req.headers.authorization + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + + // Call handler directly + const wsServer = WebSocketServer.getInstance() + const microserviceUuid = req.params.microserviceUuid + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleUserConnection(ws, req, token, microserviceUuid, true, transaction) // true = expectSystem + })() + } catch (error) { + logger.error('Error in system microservice WebSocket connection:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + microserviceUuid: req.params.microserviceUuid + })) + try { + if (ws.readyState === ws.OPEN) { + ws.close(1008, error.message || 'Connection failed') + } + } catch (closeError) { + logger.error('Error closing system microservice WebSocket:' + JSON.stringify({ + error: closeError.message, + originalError: error.message + })) + } + } + }) }, { method: 'ws', path: '/api/v3/microservices/:uuid/logs', - middleware: async (ws, req) => { + middleware: rbacMiddleware.protectWebSocket(async (ws, req) => { logger.apiReq(req) try { + // RBAC already passed, token already extracted by protectWebSocket + // Set flag to indicate RBAC authorization is complete + req._rbacAuthorized = true + + // Extract token from headers (set by protectWebSocket) const token = req.headers.authorization if (!token) { logger.error('WebSocket connection failed: Missing authentication token') try { ws.close(1008, 'Missing authentication token') } catch (error) { - logger.error('Error closing WebSocket:' + JSON.stringify({ - error: error.message, - originalError: 'Missing authentication token' - })) + logger.error('Error closing WebSocket:', error.message) } return } - // Initialize WebSocket connection for microservice logs + // Call handler directly with extracted parameters const wsServer = WebSocketServer.getInstance() - await wsServer.handleConnection(ws, req) + const microserviceUuid = req.params.uuid + const fogUuid = null + const expectSystem = false + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, expectSystem, transaction) + })() } catch (error) { logger.error('Error in microservice logs WebSocket connection:' + JSON.stringify({ error: error.message, @@ -1435,7 +1496,7 @@ module.exports = [ })) try { if (ws.readyState === ws.OPEN) { - ws.close(1008, error.message || 'Authentication failed') + ws.close(1008, error.message || 'Connection failed') } } catch (closeError) { logger.error('Error closing microservice logs WebSocket:' + JSON.stringify({ @@ -1444,6 +1505,56 @@ module.exports = [ })) } } - } + }) + }, + { + method: 'ws', + path: '/api/v3/microservices/system/:uuid/logs', + middleware: rbacMiddleware.protectWebSocket(async (ws, req) => { + logger.apiReq(req) + try { + // RBAC already passed, token already extracted by protectWebSocket + // Set flag to indicate RBAC authorization is complete + req._rbacAuthorized = true + + // Extract token from headers (set by protectWebSocket) + const token = req.headers.authorization + if (!token) { + logger.error('WebSocket connection failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + + // Call handler directly with extracted parameters + const wsServer = WebSocketServer.getInstance() + const microserviceUuid = req.params.uuid + const fogUuid = null + const expectSystem = true + await TransactionDecorator.generateTransaction(async (transaction) => { + await wsServer.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, expectSystem, transaction) + })() + } catch (error) { + logger.error('Error in system microservice logs WebSocket connection:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url, + microserviceUuid: req.params.uuid + })) + try { + if (ws.readyState === ws.OPEN) { + ws.close(1008, error.message || 'Connection failed') + } + } catch (closeError) { + logger.error('Error closing system microservice logs WebSocket:' + JSON.stringify({ + error: closeError.message, + originalError: error.message + })) + } + } + }) } ] diff --git a/src/routes/rbac.js b/src/routes/rbac.js new file mode 100644 index 00000000..1033d8a1 --- /dev/null +++ b/src/routes/rbac.js @@ -0,0 +1,697 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const constants = require('../helpers/constants') +const RbacController = require('../controllers/rbac-controller') +const ResponseDecorator = require('../decorators/response-decorator') +const Errors = require('../helpers/errors') +const logger = require('../logger') +const rbacMiddleware = require('../lib/rbac/middleware') + +module.exports = [ + // Role Management + { + method: 'get', + path: '/api/v3/roles', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const listRolesEndpoint = ResponseDecorator.handleErrors(RbacController.listRolesEndpoint, successCode, errorCodes) + const responseObject = await listRolesEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'post', + path: '/api/v3/roles', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_CREATED + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_CONFLICT, + errors: [Errors.ConflictError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const createRoleEndpoint = ResponseDecorator.handleErrors(RbacController.createRoleEndpoint, successCode, errorCodes) + const responseObject = await createRoleEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'post', + path: '/api/v3/roles/yaml', + fileInput: 'role', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_CREATED + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_CONFLICT, + errors: [Errors.ConflictError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const createRoleFromYamlEndpoint = ResponseDecorator.handleErrors(RbacController.createRoleFromYamlEndpoint, successCode, errorCodes) + const responseObject = await createRoleFromYamlEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'get', + path: '/api/v3/roles/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const getRoleEndpoint = ResponseDecorator.handleErrors(RbacController.getRoleEndpoint, successCode, errorCodes) + const responseObject = await getRoleEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/roles/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const updateRoleEndpoint = ResponseDecorator.handleErrors(RbacController.updateRoleEndpoint, successCode, errorCodes) + const responseObject = await updateRoleEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/roles/yaml/:name', + fileInput: 'role', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const updateRoleFromYamlEndpoint = ResponseDecorator.handleErrors(RbacController.updateRoleFromYamlEndpoint, successCode, errorCodes) + const responseObject = await updateRoleFromYamlEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'delete', + path: '/api/v3/roles/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const deleteRoleEndpoint = ResponseDecorator.handleErrors(RbacController.deleteRoleEndpoint, successCode, errorCodes) + const responseObject = await deleteRoleEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + // RoleBinding Management + { + method: 'get', + path: '/api/v3/rolebindings', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const listRoleBindingsEndpoint = ResponseDecorator.handleErrors(RbacController.listRoleBindingsEndpoint, successCode, errorCodes) + const responseObject = await listRoleBindingsEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'post', + path: '/api/v3/rolebindings', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_CREATED + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_CONFLICT, + errors: [Errors.ConflictError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const createRoleBindingEndpoint = ResponseDecorator.handleErrors(RbacController.createRoleBindingEndpoint, successCode, errorCodes) + const responseObject = await createRoleBindingEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'post', + path: '/api/v3/rolebindings/yaml', + fileInput: 'rolebinding', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_CREATED + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_CONFLICT, + errors: [Errors.ConflictError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const createRoleBindingFromYamlEndpoint = ResponseDecorator.handleErrors(RbacController.createRoleBindingFromYamlEndpoint, successCode, errorCodes) + const responseObject = await createRoleBindingFromYamlEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'get', + path: '/api/v3/rolebindings/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const getRoleBindingEndpoint = ResponseDecorator.handleErrors(RbacController.getRoleBindingEndpoint, successCode, errorCodes) + const responseObject = await getRoleBindingEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/rolebindings/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const updateRoleBindingEndpoint = ResponseDecorator.handleErrors(RbacController.updateRoleBindingEndpoint, successCode, errorCodes) + const responseObject = await updateRoleBindingEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/rolebindings/yaml/:name', + fileInput: 'rolebinding', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const updateRoleBindingFromYamlEndpoint = ResponseDecorator.handleErrors(RbacController.updateRoleBindingFromYamlEndpoint, successCode, errorCodes) + const responseObject = await updateRoleBindingFromYamlEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'delete', + path: '/api/v3/rolebindings/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const deleteRoleBindingEndpoint = ResponseDecorator.handleErrors(RbacController.deleteRoleBindingEndpoint, successCode, errorCodes) + const responseObject = await deleteRoleBindingEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + // ServiceAccount Management + { + method: 'get', + path: '/api/v3/serviceaccounts', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const listServiceAccountsEndpoint = ResponseDecorator.handleErrors(RbacController.listServiceAccountsEndpoint, successCode, errorCodes) + const responseObject = await listServiceAccountsEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'post', + path: '/api/v3/serviceaccounts', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_CREATED + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_CONFLICT, + errors: [Errors.ConflictError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const createServiceAccountEndpoint = ResponseDecorator.handleErrors(RbacController.createServiceAccountEndpoint, successCode, errorCodes) + const responseObject = await createServiceAccountEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'post', + path: '/api/v3/serviceaccounts/yaml', + fileInput: 'serviceaccount', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_CREATED + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_CONFLICT, + errors: [Errors.ConflictError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const createServiceAccountFromYamlEndpoint = ResponseDecorator.handleErrors(RbacController.createServiceAccountFromYamlEndpoint, successCode, errorCodes) + const responseObject = await createServiceAccountFromYamlEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'get', + path: '/api/v3/serviceaccounts/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const getServiceAccountEndpoint = ResponseDecorator.handleErrors(RbacController.getServiceAccountEndpoint, successCode, errorCodes) + const responseObject = await getServiceAccountEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/serviceaccounts/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const updateServiceAccountEndpoint = ResponseDecorator.handleErrors(RbacController.updateServiceAccountEndpoint, successCode, errorCodes) + const responseObject = await updateServiceAccountEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/serviceaccounts/yaml/:name', + fileInput: 'serviceaccount', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const updateServiceAccountFromYamlEndpoint = ResponseDecorator.handleErrors(RbacController.updateServiceAccountFromYamlEndpoint, successCode, errorCodes) + const responseObject = await updateServiceAccountFromYamlEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'delete', + path: '/api/v3/serviceaccounts/:name', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const deleteServiceAccountEndpoint = ResponseDecorator.handleErrors(RbacController.deleteServiceAccountEndpoint, successCode, errorCodes) + const responseObject = await deleteServiceAccountEndpoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + } +] diff --git a/src/routes/registries.js b/src/routes/registries.js index e2d2f8d4..3648a6ff 100644 --- a/src/routes/registries.js +++ b/src/routes/registries.js @@ -15,7 +15,7 @@ const RegistryController = require('../controllers/registry-controller') const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -37,10 +37,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const registriesEndPoint = ResponseDecorator.handleErrors(RegistryController.createRegistryEndPoint, successCode, errorCodes) const responseObject = await registriesEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -67,10 +67,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const registriesEndPoint = ResponseDecorator.handleErrors(RegistryController.getRegistriesEndPoint, successCode, errorCodes) const responseObject = await registriesEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -101,10 +101,10 @@ module.exports = [ } ] - await keycloak.protect('SRE', 'Developer')(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const registriesEndPoint = ResponseDecorator.handleErrors(RegistryController.deleteRegistryEndPoint, successCode, errorCodes) const responseObject = await registriesEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -137,7 +137,7 @@ module.exports = [ ] // Protecting for both SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateRegistryEndPoint = ResponseDecorator.handleErrors(RegistryController.updateRegistryEndPoint, successCode, errorCodes) const responseObject = await updateRegistryEndPoint(req) const user = req.kauth.grant.access_token.content.preferred_username @@ -145,6 +145,41 @@ module.exports = [ .status(responseObject.code) .send(responseObject.body) + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'get', + path: '/api/v3/registries/:id', + supportSubstitution: true, + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await rbacMiddleware.protect()(req, res, async () => { + const getRegistryEndPoint = ResponseDecorator.handleErrors(RegistryController.getRegistryEndPoint, successCode, errorCodes) + const responseObject = await getRegistryEndPoint(req) + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' + res + .status(responseObject.code) + .send(responseObject.body) + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) }) } diff --git a/src/routes/router.js b/src/routes/router.js index cc13fbbd..dbcfa0d3 100644 --- a/src/routes/router.js +++ b/src/routes/router.js @@ -15,7 +15,7 @@ const Router = require('../controllers/router-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -37,14 +37,14 @@ module.exports = [ ] // Protecting for SRE, Developer, and Viewer roles - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getRouterEndpoint = ResponseDecorator.handleErrors( Router.getRouterEndPoint, successCode, errorCodes ) const responseObject = await getRouterEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -73,7 +73,7 @@ module.exports = [ ] // Protecting for SRE role - await keycloak.protect('SRE')(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const upsertDefaultRouter = ResponseDecorator.handleErrors( Router.upsertDefaultRouter, successCode, diff --git a/src/routes/routing.js b/src/routes/routing.js index e44c9d2a..311226ad 100644 --- a/src/routes/routing.js +++ b/src/routes/routing.js @@ -15,7 +15,7 @@ const Routing = require('../controllers/routing-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -37,14 +37,14 @@ module.exports = [ ] // Protecting for SRE , Developer and Viewer roles - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getRouterEndpoint = ResponseDecorator.handleErrors( Routing.getRoutingsEndPoint, successCode, errorCodes ) const responseObject = await getRouterEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -72,14 +72,14 @@ module.exports = [ ] // Protecting for SRE, Developer, and Viewer roles - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getRouterEndpoint = ResponseDecorator.handleErrors( Routing.getRoutingEndPoint, successCode, errorCodes ) const responseObject = await getRouterEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -116,14 +116,14 @@ module.exports = [ ] // Protecting for SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createRoutingEndpoint = ResponseDecorator.handleErrors( Routing.createRoutingEndpoint, successCode, errorCodes ) const responseObject = await createRoutingEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -156,14 +156,14 @@ module.exports = [ ] // Protecting for SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateRoutingEndpoint = ResponseDecorator.handleErrors( Routing.updateRoutingEndpoint, successCode, errorCodes ) const responseObject = await updateRoutingEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -191,7 +191,7 @@ module.exports = [ ] // Protecting for SRE and Developer roles - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteRoutingEndpoint = ResponseDecorator.handleErrors( Routing.deleteRoutingEndpoint, successCode, diff --git a/src/routes/secret.js b/src/routes/secret.js index b6c6f55a..cda187aa 100644 --- a/src/routes/secret.js +++ b/src/routes/secret.js @@ -16,7 +16,7 @@ const SecretController = require('../controllers/secret-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -41,10 +41,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createSecretEndpoint = ResponseDecorator.handleErrors(SecretController.createSecretEndpoint, successCode, errorCodes) const responseObject = await createSecretEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -76,10 +76,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createSecretFromYamlEndpoint = ResponseDecorator.handleErrors(SecretController.createSecretFromYamlEndpoint, successCode, errorCodes) const responseObject = await createSecretFromYamlEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -110,10 +110,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateSecretEndpoint = ResponseDecorator.handleErrors(SecretController.updateSecretEndpoint, successCode, errorCodes) const responseObject = await updateSecretEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -145,10 +145,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateSecretFromYamlEndpoint = ResponseDecorator.handleErrors(SecretController.updateSecretFromYamlEndpoint, successCode, errorCodes) const responseObject = await updateSecretFromYamlEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -175,10 +175,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getSecretEndpoint = ResponseDecorator.handleErrors(SecretController.getSecretEndpoint, successCode, errorCodes) const responseObject = await getSecretEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -201,10 +201,10 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listSecretsEndpoint = ResponseDecorator.handleErrors(SecretController.listSecretsEndpoint, successCode, errorCodes) const responseObject = await listSecretsEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -231,7 +231,7 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteSecretEndpoint = ResponseDecorator.handleErrors(SecretController.deleteSecretEndpoint, successCode, errorCodes) const responseObject = await deleteSecretEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/service.js b/src/routes/service.js index e3f198ba..64163af7 100644 --- a/src/routes/service.js +++ b/src/routes/service.js @@ -16,7 +16,7 @@ const ServiceController = require('../controllers/service-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -33,14 +33,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const listServicesEndpoint = ResponseDecorator.handleErrors( ServiceController.listServicesEndpoint, successCode, errorCodes ) const responseObject = await listServicesEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -67,14 +67,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const getServiceEndpoint = ResponseDecorator.handleErrors( ServiceController.getServiceEndpoint, successCode, errorCodes ) const responseObject = await getServiceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -105,14 +105,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createServiceEndpoint = ResponseDecorator.handleErrors( ServiceController.createServiceEndpoint, successCode, errorCodes ) const responseObject = await createServiceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -143,14 +143,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateServiceEndpoint = ResponseDecorator.handleErrors( ServiceController.updateServiceEndpoint, successCode, errorCodes ) const responseObject = await updateServiceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -177,14 +177,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const deleteServiceEndpoint = ResponseDecorator.handleErrors( ServiceController.deleteServiceEndpoint, successCode, errorCodes ) const responseObject = await deleteServiceEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -216,14 +216,14 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const createServiceYAMLEndpoint = ResponseDecorator.handleErrors( ServiceController.createServiceYAMLEndpoint, successCode, errorCodes ) const responseObject = await createServiceYAMLEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -255,7 +255,7 @@ module.exports = [ } ] - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const updateServiceYAMLEndpoint = ResponseDecorator.handleErrors( ServiceController.updateServiceYAMLEndpoint, successCode, diff --git a/src/routes/tunnel.js b/src/routes/tunnel.js index 40c3b622..c9f115b5 100644 --- a/src/routes/tunnel.js +++ b/src/routes/tunnel.js @@ -15,7 +15,7 @@ const TunnelController = require('../controllers/tunnel-controller') const ResponseDecorator = require('../decorators/response-decorator') const Errors = require('../helpers/errors') const logger = require('../logger') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -41,14 +41,14 @@ module.exports = [ ] // Protecting for SRE and Developer roles - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const tunnelEndPoint = ResponseDecorator.handleErrors( TunnelController.manageTunnelEndPoint, successCode, errorCodes ) const responseObject = await tunnelEndPoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -76,7 +76,7 @@ module.exports = [ ] // Protecting for SRE and Developer roles - await keycloak.protect(['SRE'])(req, res, async () => { + await rbacMiddleware.protect()(req, res, async () => { const tunnelEndPoint = ResponseDecorator.handleErrors( TunnelController.getTunnelEndPoint, successCode, diff --git a/src/routes/volumeMount.js b/src/routes/volumeMount.js index 99dee9a5..a8af91cb 100644 --- a/src/routes/volumeMount.js +++ b/src/routes/volumeMount.js @@ -15,7 +15,7 @@ const VolumeMountController = require('../controllers/volume-mount-controller') const ResponseDecorator = require('../decorators/response-decorator') const logger = require('../logger') const Errors = require('../helpers/errors') -const keycloak = require('../config/keycloak.js').initKeycloak() +const rbacMiddleware = require('../lib/rbac/middleware') module.exports = [ { @@ -32,11 +32,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const getVolumeMountsEndpoint = ResponseDecorator.handleErrors(VolumeMountController.listVolumeMountsEndpoint, successCode, errorCodes) const responseObject = await getVolumeMountsEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -63,11 +63,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const getVolumeMountEndpoint = ResponseDecorator.handleErrors(VolumeMountController.getVolumeMountEndpoint, successCode, errorCodes) const responseObject = await getVolumeMountEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -99,11 +99,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const updateVolumeMountEndpoint = ResponseDecorator.handleErrors(VolumeMountController.updateVolumeMountEndpoint, successCode, errorCodes) const responseObject = await updateVolumeMountEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -134,11 +134,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const deleteVolumeMountEndpoint = ResponseDecorator.handleErrors(VolumeMountController.deleteVolumeMountEndpoint, successCode, errorCodes) const responseObject = await deleteVolumeMountEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -166,11 +166,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const createVolumeMountEndpoint = ResponseDecorator.handleErrors(VolumeMountController.createVolumeMountEndpoint, successCode, errorCodes) const responseObject = await createVolumeMountEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -198,11 +198,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const createVolumeMountYamlEndpoint = ResponseDecorator.handleErrors(VolumeMountController.createVolumeMountYamlEndpoint, successCode, errorCodes) const responseObject = await createVolumeMountYamlEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -234,11 +234,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const updateVolumeMountYamlEndpoint = ResponseDecorator.handleErrors(VolumeMountController.updateVolumeMountYamlEndpoint, successCode, errorCodes) const responseObject = await updateVolumeMountYamlEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -265,11 +265,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const getVolumeMountLinkEndpoint = ResponseDecorator.handleErrors(VolumeMountController.getVolumeMountLinkEndpoint, successCode, errorCodes) const responseObject = await getVolumeMountLinkEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -296,11 +296,11 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const linkVolumeMountEndpoint = ResponseDecorator.handleErrors(VolumeMountController.linkVolumeMountEndpoint, successCode, errorCodes) const responseObject = await linkVolumeMountEndpoint(req) - const user = req.kauth.grant.access_token.content.preferred_username + const user = req.kauth && req.kauth.grant && req.kauth.grant.access_token ? req.kauth.grant.access_token.content.preferred_username : 'system' res .status(responseObject.code) .send(responseObject.body) @@ -327,8 +327,8 @@ module.exports = [ } ] - // Add keycloak.protect() middleware to protect the route for SRE role - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + // Add rbacMiddleware.protect middleware to protect the route for SRE role + await rbacMiddleware.protect()(req, res, async () => { const unlinkVolumeMountEndpoint = ResponseDecorator.handleErrors(VolumeMountController.unlinkVolumeMountEndpoint, successCode, errorCodes) const responseObject = await unlinkVolumeMountEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/schemas/cluster-controller.js b/src/schemas/cluster-controller.js new file mode 100644 index 00000000..e3fb335a --- /dev/null +++ b/src/schemas/cluster-controller.js @@ -0,0 +1,28 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const clusterControllerUpdate = { + 'id': '/clusterControllerUpdate', + 'type': 'object', + 'properties': { + 'host': { + 'type': 'string' + } + }, + 'additionalProperties': false +} + +module.exports = { + mainSchemas: [clusterControllerUpdate], + innerSchemas: [] +} diff --git a/src/schemas/microservice.js b/src/schemas/microservice.js index 435e20b5..230cb847 100644 --- a/src/schemas/microservice.js +++ b/src/schemas/microservice.js @@ -82,6 +82,21 @@ const microserviceCreate = { 'healthCheck': { 'type': 'object', 'properties': { '$ref': '/microserviceHealthCheck' } + }, + 'serviceAccount': { + 'type': 'object', + 'properties': { + 'roleRef': { + 'type': 'object', + 'properties': { + 'kind': { 'type': 'string' }, + 'name': { 'type': 'string' }, + 'apiGroup': { 'type': 'string' } + }, + 'required': ['kind', 'name'] + } + }, + 'additionalProperties': false } }, 'required': ['name'], @@ -156,6 +171,21 @@ const microserviceUpdate = { 'healthCheck': { 'type': 'object', 'properties': { '$ref': '/microserviceHealthCheck' } + }, + 'serviceAccount': { + 'type': 'object', + 'properties': { + 'roleRef': { + 'type': 'object', + 'properties': { + 'kind': { 'type': 'string' }, + 'name': { 'type': 'string' }, + 'apiGroup': { 'type': 'string' } + }, + 'required': ['kind', 'name'] + } + }, + 'additionalProperties': false } }, 'additionalProperties': true diff --git a/src/schemas/rbac.js b/src/schemas/rbac.js new file mode 100644 index 00000000..f361279f --- /dev/null +++ b/src/schemas/rbac.js @@ -0,0 +1,248 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const { nameRegex } = require('./utils/utils') + +// Inner Schema: RBAC Rule +const rbacRule = { + id: '/rbacRule', + type: 'object', + required: ['apiGroups', 'resources', 'verbs'], + properties: { + apiGroups: { + type: 'array', + items: { type: 'string' }, + minItems: 1 + }, + resources: { + type: 'array', + items: { type: 'string' }, + minItems: 1 + }, + verbs: { + type: 'array', + items: { + type: 'string', + enum: ['get', 'list', 'create', 'update', 'patch', 'delete', '*'] + }, + minItems: 1 + }, + resourceNames: { + type: 'array', + items: { type: 'string' } + } + }, + additionalProperties: false +} + +// Inner Schema: Role Reference +const roleRef = { + id: '/roleRef', + type: 'object', + required: ['kind', 'name'], + properties: { + kind: { + type: 'string', + enum: ['Role'] + }, + name: { + type: 'string', + minLength: 1 + }, + apiGroup: { + type: 'string' + } + }, + additionalProperties: false +} + +// Inner Schema: Subject +const subject = { + id: '/subject', + type: 'object', + required: ['kind', 'name'], + properties: { + kind: { + type: 'string', + enum: ['User', 'Group', 'ServiceAccount'] + }, + name: { + type: 'string', + minLength: 1 + }, + apiGroup: { + type: 'string' + } + }, + additionalProperties: false +} + +// Main Schema: Role Create +const roleCreate = { + id: '/roleCreate', + type: 'object', + required: ['name', 'rules'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: nameRegex + }, + kind: { + type: 'string', + enum: ['Role'] + }, + rules: { + type: 'array', + minItems: 1, + items: { $ref: '/rbacRule' } + } + }, + additionalProperties: false +} + +// Main Schema: Role Update +const roleUpdate = { + id: '/roleUpdate', + type: 'object', + required: ['rules'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: nameRegex + }, + kind: { + type: 'string', + enum: ['Role'] + }, + rules: { + type: 'array', + minItems: 1, + items: { $ref: '/rbacRule' } + } + }, + additionalProperties: false +} + +// Main Schema: RoleBinding Create +const roleBindingCreate = { + id: '/roleBindingCreate', + type: 'object', + required: ['name', 'roleRef', 'subjects'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: nameRegex + }, + kind: { + type: 'string', + enum: ['RoleBinding'] + }, + roleRef: { + $ref: '/roleRef' + }, + subjects: { + type: 'array', + minItems: 1, + items: { $ref: '/subject' } + } + }, + additionalProperties: false +} + +// Main Schema: RoleBinding Update +const roleBindingUpdate = { + id: '/roleBindingUpdate', + type: 'object', + required: ['roleRef', 'subjects'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: nameRegex + }, + kind: { + type: 'string', + enum: ['RoleBinding'] + }, + roleRef: { + $ref: '/roleRef' + }, + subjects: { + type: 'array', + minItems: 1, + items: { $ref: '/subject' } + } + }, + additionalProperties: false +} + +// Main Schema: ServiceAccount Create +const serviceAccountCreate = { + id: '/serviceAccountCreate', + type: 'object', + required: ['name', 'roleRef'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: nameRegex + }, + roleRef: { + $ref: '/roleRef' + } + }, + additionalProperties: false +} + +// Main Schema: ServiceAccount Update +const serviceAccountUpdate = { + id: '/serviceAccountUpdate', + type: 'object', + required: ['name', 'roleRef'], + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: nameRegex + }, + roleRef: { + $ref: '/roleRef' + } + }, + additionalProperties: false +} + +module.exports = { + mainSchemas: [ + roleCreate, + roleUpdate, + roleBindingCreate, + roleBindingUpdate, + serviceAccountCreate, + serviceAccountUpdate + ], + innerSchemas: [ + rbacRule, + roleRef, + subject + ] +} diff --git a/src/server.js b/src/server.js index f5b1d3f8..f76a5d12 100755 --- a/src/server.js +++ b/src/server.js @@ -158,9 +158,8 @@ initialize().then(() => { jobs.forEach((job) => job.run()) }) - // Initialize WebSocket server - const wsConfig = config.get('server.webSocket') - const wsServer = new WebSocketServer(wsConfig) + // Initialize WebSocket server (use singleton to ensure routes are available) + const wsServer = WebSocketServer.getInstance() wsServer.initialize(apiServer) logger.info(`==> 🌎 Webscoker API server listening on port ${ports.api}. Open up ws://localhost:${ports.api}/.`) registerServers(apiServer, viewerServer) @@ -193,9 +192,8 @@ initialize().then(() => { jobs.forEach((job) => job.run()) }) - // Initialize WebSocket server with SSL - const wsConfig = config.get('server.webSocket') - const wsServer = new WebSocketServer(wsConfig) + // Initialize WebSocket server with SSL (use singleton to ensure routes are available) + const wsServer = WebSocketServer.getInstance() wsServer.initialize(apiServer) logger.info(`==> 🌎 WSS API server listening on port ${ports.api}. Open up wss://localhost:${ports.api}/.`) diff --git a/src/services/agent-service.js b/src/services/agent-service.js index fac9bee5..427687a5 100644 --- a/src/services/agent-service.js +++ b/src/services/agent-service.js @@ -48,6 +48,7 @@ const SecretManager = require('../data/managers/secret-manager') const ConfigMapManager = require('../data/managers/config-map-manager') const MicroserviceLogStatusManager = require('../data/managers/microservice-log-status-manager') const FogLogStatusManager = require('../data/managers/fog-log-status-manager') +const RbacRoleManager = require('../data/managers/rbac-role-manager') const IncomingForm = formidable.IncomingForm const CHANGE_TRACKING_DEFAULT = {} @@ -59,6 +60,8 @@ for (const key of CHANGE_TRACKING_KEYS) { const agentProvision = async function (provisionData, transaction) { await Validator.validate(provisionData, Validator.schemas.agentProvision) + const namespace = process.env.CONTROLLER_NAMESPACE || config.get('app.namespace', 'datasance') + const provision = await FogProvisionKeyManager.findOne({ provisionKey: provisionData.key }, transaction) @@ -104,7 +107,8 @@ const agentProvision = async function (provisionData, transaction) { return { uuid: fog.uuid, - privateKey: keyPair.privateKey + privateKey: keyPair.privateKey, + namespace: namespace } } @@ -300,7 +304,7 @@ const updateAgentStatus = async function (agentStatus, fog, transaction) { if (agentStatus.warningMessage.includes('HW signature changed') || agentStatus.warningMessage.includes('HW signature mismatch')) { fogStatus.securityStatus = 'WARNING' - fogStatus.securityViolationInfo = 'HW signature mismatch' + fogStatus.securityViolationInfo = 'Auto deprovisioned by Agent. HW signature mismatch' } else { fogStatus.securityStatus = 'OK' fogStatus.securityViolationInfo = 'No violation' @@ -345,6 +349,37 @@ const _mapExtraHost = function (extraHost) { return `${extraHost.name}:${extraHost.value}` } +/** + * Resolve service account rules from roleRef + * @param {Object} serviceAccount - Service account object (from relationship or lookup) + * @param {Object} transaction - Database transaction + * @returns {Object|null} Service account with resolved rules or null if not found + */ +async function _resolveServiceAccountRules (serviceAccount, transaction) { + try { + // If serviceAccount is already loaded from relationship, use it + // Otherwise, it might be null/undefined + if (!serviceAccount || !serviceAccount.roleRef) { + return null + } + + // Get role from roleRef (check system roles first, then database) + const role = await RbacRoleManager.getRoleWithRules(serviceAccount.roleRef.name, transaction) + if (!role) { + return null + } + + return { + name: serviceAccount.name, + roleRef: serviceAccount.roleRef, + rules: role.rules + } + } catch (error) { + // If service account doesn't exist or error, return null + return null + } +} + const getAgentMicroservices = async function (fog, transaction) { const microservices = await MicroserviceManager.findAllActiveApplicationMicroservices(fog.uuid, transaction) @@ -447,8 +482,21 @@ const getAgentMicroservices = async function (fog, transaction) { schedule: microservice.schedule } - response.push(responseMicroservice) + // Resolve service account with rules from relationship + const serviceAccountData = await _resolveServiceAccountRules(microservice.serviceAccount, transaction) + if (serviceAccountData) { + responseMicroservice.serviceAccount = { + name: serviceAccountData.name, + roleRef: serviceAccountData.roleRef, + rules: serviceAccountData.rules + } + } else { + // If service account doesn't exist, create default one or leave null + // For now, we'll leave it null - the agent should handle this gracefully + responseMicroservice.serviceAccount = null + } + response.push(responseMicroservice) await MicroserviceManager.update({ uuid: microservice.uuid }, { diff --git a/src/services/cluster-controller-service.js b/src/services/cluster-controller-service.js new file mode 100644 index 00000000..85039018 --- /dev/null +++ b/src/services/cluster-controller-service.js @@ -0,0 +1,169 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const os = require('os') +const AppHelper = require('../helpers/app-helper') +const ErrorMessages = require('../helpers/error-messages') +const Errors = require('../helpers/errors') +const ClusterControllerManager = require('../data/managers/cluster-controller-manager') +const logger = require('../logger') +const TransactionDecorator = require('../decorators/transaction-decorator') +const Validator = require('../schemas') + +// Store current controller UUID in module scope +let currentControllerUuid = null + +async function initializeControllerUuid (transaction) { + try { + const host = process.env.CONTROLLER_HOST || process.env.HOSTNAME || os.hostname() + const processId = process.pid + + // Try to find existing active controller on this host + const existing = await ClusterControllerManager.findOne({ + host, + isActive: true + }, transaction) + + let uuid + + if (existing) { + // Same host restarting - reuse UUID and update heartbeat + uuid = existing.uuid + logger.info(`Reusing existing UUID for host ${host}: ${uuid}`) + + await ClusterControllerManager.update( + { uuid }, + { + lastHeartbeat: new Date(), + isActive: true, + processId: processId + }, + transaction + ) + } else { + // New instance - generate UUID + uuid = AppHelper.generateUUID() + logger.info(`Generated new controller UUID: ${uuid} for host: ${host}`) + + await ClusterControllerManager.create({ + uuid, + host, + processId: processId, + lastHeartbeat: new Date(), + isActive: true + }, transaction) + } + + // Store UUID in module scope + currentControllerUuid = uuid + + return uuid + } catch (error) { + logger.error(`Failed to initialize controller UUID: ${error.message}`) + throw error + } +} + +function getCurrentControllerUuid () { + return currentControllerUuid +} + +async function updateHeartbeat (uuid, transaction) { + if (!uuid) { + uuid = currentControllerUuid + } + + if (!uuid) { + logger.warn('Cannot update heartbeat: controller UUID not initialized') + return + } + + await ClusterControllerManager.update( + { uuid }, + { + lastHeartbeat: new Date(), + isActive: true + }, + transaction + ) +} + +async function listClusterControllers (transaction) { + const controllers = await ClusterControllerManager.findAll({}, transaction) + return controllers.map(controller => ({ + uuid: controller.uuid, + host: controller.host, + processId: controller.processId, + lastHeartbeat: controller.lastHeartbeat, + isActive: controller.isActive, + createdAt: controller.createdAt, + updatedAt: controller.updatedAt + })) +} + +async function getClusterController (uuid, transaction) { + const controller = await ClusterControllerManager.findOne({ uuid }, transaction) + if (!controller) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.CLUSTER_CONTROLLER_NOT_FOUND)) + } + return { + uuid: controller.uuid, + host: controller.host, + processId: controller.processId, + lastHeartbeat: controller.lastHeartbeat, + isActive: controller.isActive, + createdAt: controller.createdAt, + updatedAt: controller.updatedAt + } +} + +async function updateClusterController (uuid, data, transaction) { + await Validator.validate(data, Validator.schemas.clusterControllerUpdate) + + const controller = await ClusterControllerManager.findOne({ uuid }, transaction) + if (!controller) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.CLUSTER_CONTROLLER_NOT_FOUND)) + } + + const updateData = AppHelper.deleteUndefinedFields({ + host: data.host + }) + + await ClusterControllerManager.update({ uuid }, updateData, transaction) + return getClusterController(uuid, transaction) +} + +async function deleteClusterController (uuid, transaction) { + const controller = await ClusterControllerManager.findOne({ uuid }, transaction) + if (!controller) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.CLUSTER_CONTROLLER_NOT_FOUND)) + } + + // Soft delete: mark as inactive instead of deleting (for audit trail) + await ClusterControllerManager.update( + { uuid }, + { isActive: false }, + transaction + ) + return { uuid } +} + +module.exports = { + initializeControllerUuid: TransactionDecorator.generateTransaction(initializeControllerUuid), + getCurrentControllerUuid, + updateHeartbeat: TransactionDecorator.generateTransaction(updateHeartbeat), + listClusterControllers: TransactionDecorator.generateTransaction(listClusterControllers), + getClusterController: TransactionDecorator.generateTransaction(getClusterController), + updateClusterController: TransactionDecorator.generateTransaction(updateClusterController), + deleteClusterController: TransactionDecorator.generateTransaction(deleteClusterController) +} diff --git a/src/services/event-service.js b/src/services/event-service.js index 08dbc101..1aad64b1 100644 --- a/src/services/event-service.js +++ b/src/services/event-service.js @@ -48,6 +48,7 @@ function extractResourceType (path) { { pattern: /^\/api\/v3\/applicationTemplates/, type: 'applicationTemplate' }, { pattern: /^\/api\/v3\/catalog/, type: 'catalog' }, { pattern: /^\/api\/v3\/controller/, type: 'controller' }, + { pattern: /^\/api\/v3\/cluster/, type: 'cluster' }, { pattern: /^\/api\/v3\/users/, type: 'user' }, { pattern: /^\/api\/v3\/capabilities/, type: 'capabilities' }, { pattern: /^\/api\/v3\/events/, type: 'event' } diff --git a/src/services/microservices-service.js b/src/services/microservices-service.js index aaa3d967..a8d6b285 100644 --- a/src/services/microservices-service.js +++ b/src/services/microservices-service.js @@ -41,6 +41,8 @@ const ServiceServices = require('./services-service') const ConfigMapManager = require('../data/managers/config-map-manager') const SecretManager = require('../data/managers/secret-manager') const VolumeMountService = require('./volume-mount-service') +const RbacServiceAccountManager = require('../data/managers/rbac-service-account-manager') +const RbacRoleManager = require('../data/managers/rbac-role-manager') const Op = require('sequelize').Op const FogManager = require('../data/managers/iofog-manager') @@ -79,6 +81,64 @@ async function _setSubTags (microserviceModel, tagsArray, transaction) { } } +/** + * Create or update service account for a microservice + * @param {string} microserviceName - Name of the microservice (used as service account name) + * @param {string|null|undefined} roleRefName - Name of the role to reference (defaults to 'microservice' if not provided) + * @param {Object} transaction - Database transaction + * @returns {Object} Service account object + */ +async function _createOrUpdateServiceAccountForMicroservice (microserviceName, roleRefName, transaction) { + // Default to 'microservice' role if not provided, null, undefined, or empty string + const roleName = (roleRefName && typeof roleRefName === 'string' && roleRefName.trim() !== '') ? roleRefName : 'microservice' + + // Validate role exists (check system roles first, then database) + const role = await RbacRoleManager.getRoleWithRules(roleName, transaction) + if (!role) { + throw new Errors.ValidationError(`Referenced role '${roleName}' does not exist`) + } + + // Prepare roleRef object + const roleRef = { + kind: 'Role', + name: roleName + } + + // Check if service account already exists + const existingServiceAccount = await RbacServiceAccountManager.findOne({ name: microserviceName }, transaction) + + if (existingServiceAccount) { + // Update existing service account + const updated = await RbacServiceAccountManager.updateServiceAccount(microserviceName, { roleRef }, transaction) + return updated + } else { + // Create new service account + const created = await RbacServiceAccountManager.createServiceAccount({ + name: microserviceName, + roleRef + }, transaction) + return created + } +} + +/** + * Delete service account for a microservice + * @param {string} microserviceName - Name of the microservice (used as service account name) + * @param {Object} transaction - Database transaction + */ +async function _deleteServiceAccountForMicroservice (microserviceName, transaction) { + try { + await RbacServiceAccountManager.deleteServiceAccount(microserviceName, transaction) + } catch (error) { + // Gracefully handle not found errors (service account might not exist) + if (error.name !== 'NotFoundError') { + throw error + } + // Log but don't fail if service account doesn't exist + logger.warn(`Service account '${microserviceName}' not found during microservice deletion, continuing...`) + } +} + async function listMicroservicesEndPoint (opt, isCLI, transaction) { // API retro compatibility const { applicationName, flowId } = opt @@ -442,6 +502,26 @@ async function createMicroserviceEndPoint (microserviceData, isCLI, transaction) await _createMicroserviceStatus(microservice, transaction) await _createMicroserviceExecStatus(microservice, transaction) + // Create service account for microservice (always create, use default 'microservice' role if not specified) + let roleRefName = null + if (microserviceData.serviceAccount && + microserviceData.serviceAccount.roleRef && + microserviceData.serviceAccount.roleRef.name) { + roleRefName = microserviceData.serviceAccount.roleRef.name + } + try { + const serviceAccount = await _createOrUpdateServiceAccountForMicroservice(microservice.name, roleRefName, transaction) + // Update microservice with service account ID + await MicroserviceManager.update( + { uuid: microservice.uuid }, + { serviceAccountId: serviceAccount.id }, + transaction + ) + } catch (error) { + logger.error(`Failed to create service account for microservice ${microservice.name}:`, error.message) + throw error + } + const res = { uuid: microservice.uuid, name: microservice.name @@ -858,6 +938,35 @@ async function updateSystemMicroserviceEndPoint (microserviceUuid, microserviceD }, microserviceStatus, transaction) } + // Always ensure service account exists for microservice + // If serviceAccount field is provided, use it; otherwise ensure default service account exists + const microserviceName = updatedMicroservice.name || microservice.name + let roleRefName = null + + if (microserviceData.serviceAccount !== undefined) { + // Handle null, empty object, or valid roleRef + if (microserviceData.serviceAccount && + microserviceData.serviceAccount.roleRef && + microserviceData.serviceAccount.roleRef.name) { + roleRefName = microserviceData.serviceAccount.roleRef.name + } + // If serviceAccount is null or empty object, roleRefName will be null and default to 'microservice' role + } + // If serviceAccount field is not provided, roleRefName stays null and will default to 'microservice' role + + try { + const serviceAccount = await _createOrUpdateServiceAccountForMicroservice(microserviceName, roleRefName, transaction) + // Update microservice with service account ID + await MicroserviceManager.update( + { uuid: updatedMicroservice.uuid }, + { serviceAccountId: serviceAccount.id }, + transaction + ) + } catch (error) { + logger.error(`Failed to update service account for microservice ${microserviceName}:`, error.message) + throw error + } + if (changeTrackingEnabled) { await ChangeTrackingService.update(microservice.iofogUuid, ChangeTrackingService.events.microserviceRouting, transaction) await ChangeTrackingService.update(updatedMicroservice.iofogUuid, ChangeTrackingService.events.microserviceRouting, transaction) @@ -1149,6 +1258,35 @@ async function updateMicroserviceEndPoint (microserviceUuid, microserviceData, i }, microserviceStatus, transaction) } + // Always ensure service account exists for microservice + // If serviceAccount field is provided, use it; otherwise ensure default service account exists + const microserviceName = updatedMicroservice.name || microservice.name + let roleRefName = null + + if (microserviceData.serviceAccount !== undefined) { + // Handle null, empty object, or valid roleRef + if (microserviceData.serviceAccount && + microserviceData.serviceAccount.roleRef && + microserviceData.serviceAccount.roleRef.name) { + roleRefName = microserviceData.serviceAccount.roleRef.name + } + // If serviceAccount is null or empty object, roleRefName will be null and default to 'microservice' role + } + // If serviceAccount field is not provided, roleRefName stays null and will default to 'microservice' role + + try { + const serviceAccount = await _createOrUpdateServiceAccountForMicroservice(microserviceName, roleRefName, transaction) + // Update microservice with service account ID + await MicroserviceManager.update( + { uuid: updatedMicroservice.uuid }, + { serviceAccountId: serviceAccount.id }, + transaction + ) + } catch (error) { + logger.error(`Failed to update service account for system microservice ${microserviceName}:`, error.message) + throw error + } + if (changeTrackingEnabled) { await ChangeTrackingService.update(microservice.iofogUuid, ChangeTrackingService.events.microserviceRouting, transaction) await ChangeTrackingService.update(updatedMicroservice.iofogUuid, ChangeTrackingService.events.microserviceRouting, transaction) @@ -1389,6 +1527,10 @@ async function deleteMicroserviceEndPoint (microserviceUuid, microserviceData, i logger.info(`Deleting service ${existingService.name}`) await ServiceServices.deleteServiceEndpoint(existingService.name, transaction) } + + // Delete service account for microservice + await _deleteServiceAccountForMicroservice(microservice.name, transaction) + await deleteMicroserviceWithRoutesAndPortMappings(microservice, transaction) await _updateChangeTracking(false, microservice.iofogUuid, transaction) } @@ -2298,6 +2440,9 @@ async function deleteMicroserviceWithRoutesAndPortMappings (microservice, transa await MicroservicePortService.deletePortMappings(microservice, transaction) + // Delete service account for microservice (safety net in case it wasn't deleted earlier) + await _deleteServiceAccountForMicroservice(microservice.name, transaction) + await MicroserviceManager.delete({ uuid: microservice.uuid }, transaction) diff --git a/src/services/rbac-service.js b/src/services/rbac-service.js new file mode 100644 index 00000000..7262a4bd --- /dev/null +++ b/src/services/rbac-service.js @@ -0,0 +1,292 @@ +/* + * ******************************************************************************* + * * Copyright (c) 2023 Datasance Teknoloji A.S. + * * + * * This program and the accompanying materials are made available under the + * * terms of the Eclipse Public License v. 2.0 which is available at + * * http://www.eclipse.org/legal/epl-2.0 + * * + * * SPDX-License-Identifier: EPL-2.0 + * ******************************************************************************* + * + */ + +const RbacRoleManager = require('../data/managers/rbac-role-manager') +const RbacRoleBindingManager = require('../data/managers/rbac-role-binding-manager') +const RbacServiceAccountManager = require('../data/managers/rbac-service-account-manager') +const MicroserviceManager = require('../data/managers/microservice-manager') +const TransactionDecorator = require('../decorators/transaction-decorator') +const Errors = require('../helpers/errors') +const Validator = require('../schemas/index') +const ChangeTrackingService = require('./change-tracking-service') +const logger = require('../logger') + +/** + * Validate that role name does not match a system role + * @param {string} roleName - Role name to validate + * @throws {Errors.ValidationError} If role name matches a system role + */ +function validateNotSystemRole (roleName) { + if (RbacRoleManager.isSystemRole(roleName)) { + throw new Errors.ValidationError(`Cannot use role name '${roleName}'. '${roleName}' is a system role and cannot be created or used.`) + } +} + +/** + * Helper function to notify microservices when a service account is updated + * Finds all microservices linked to the service account and triggers change tracking + * @param {number} serviceAccountId - ID of the updated service account + * @param {object} transaction - Database transaction + */ +async function _notifyMicroservicesForServiceAccountUpdate (serviceAccountId, transaction) { + try { + const microservices = await MicroserviceManager.findAll({ + serviceAccountId: serviceAccountId + }, transaction) + + if (microservices && microservices.length > 0) { + const iofogUuids = microservices + .map(ms => ms.iofogUuid) + .filter(uuid => uuid !== null && uuid !== undefined) + + for (const iofogUuid of iofogUuids) { + try { + await ChangeTrackingService.update(iofogUuid, ChangeTrackingService.events.microserviceFull, transaction) + } catch (error) { + logger.error(`Failed to update change tracking for fog ${iofogUuid} after service account update:`, error.message) + } + } + } + } catch (error) { + logger.error(`Failed to notify microservices for service account update (ID: ${serviceAccountId}):`, error.message) + } +} + +// Role Management +async function listRolesEndpoint (transaction) { + const roles = await RbacRoleManager.listRoles(transaction) + return { + roles: roles + } +} + +async function getRoleEndpoint (name, transaction) { + const role = await RbacRoleManager.getRoleWithRules(name, transaction) + if (!role) { + throw new Errors.NotFoundError(`Role '${name}' not found`) + } + return { + role: role + } +} + +async function createRoleEndpoint (roleData, transaction) { + // Validate schema + await Validator.validate(roleData, Validator.schemas.roleCreate) + + // Validate role name does not match system role + validateNotSystemRole(roleData.name) + + const role = await RbacRoleManager.createRole(roleData, transaction) + return { + role: role + } +} + +async function updateRoleEndpoint (name, roleData, transaction) { + // Validate schema + await Validator.validate(roleData, Validator.schemas.roleUpdate) + + // Prevent updating system roles + if (RbacRoleManager.isSystemRole(name)) { + throw new Errors.ValidationError(`Cannot update role '${name}'. '${name}' is a system role and cannot be modified.`) + } + + // Prevent renaming to system role names + if (roleData.name) { + validateNotSystemRole(roleData.name) + } + + // Get the role before update to get its ID - use findOne to get actual database model + const oldRole = await RbacRoleManager.findOne({ name }, transaction) + if (!oldRole) { + throw new Errors.NotFoundError(`Role '${name}' not found`) + } + + const role = await RbacRoleManager.updateRole(name, roleData, transaction) + // Get the updated role to get its ID (in case name changed) - use findOne to get actual database model + const updatedRoleName = roleData.name || name + const updatedRole = await RbacRoleManager.findOne({ name: updatedRoleName }, transaction) + if (!updatedRole) { + throw new Errors.NotFoundError(`Role '${updatedRoleName}' not found after update`) + } + + const roleId = updatedRole.id + + // Only query for bindings/service accounts if we have a valid roleId + // System roles don't have database IDs, but we already prevent updating system roles above + if (roleId != null) { + // Find all role bindings that reference this role using roleId for efficient querying + const bindings = await RbacRoleBindingManager.findAll({ roleId: roleId }, transaction) + for (const binding of bindings) { + // Trigger update to refresh cache and ensure roleId is set + await RbacRoleBindingManager.updateRoleBinding(binding.name, { + roleRef: binding.roleRef + }, transaction) + } + + // Find all service accounts that reference this role using roleId for efficient querying + const serviceAccounts = await RbacServiceAccountManager.findAll({ roleId: roleId }, transaction) + for (const sa of serviceAccounts) { + // Trigger update to refresh cache and ensure roleId is set + await RbacServiceAccountManager.updateServiceAccount(sa.name, { + roleRef: sa.roleRef + }, transaction) + // Notify linked microservices + await _notifyMicroservicesForServiceAccountUpdate(sa.id, transaction) + } + } + + return { + role: role + } +} + +async function deleteRoleEndpoint (name, transaction) { + await RbacRoleManager.deleteRole(name, transaction) + return { + message: `Role '${name}' deleted successfully` + } +} + +// RoleBinding Management +async function listRoleBindingsEndpoint (transaction) { + const bindings = await RbacRoleBindingManager.listRoleBindings(transaction) + return { + bindings: bindings + } +} + +async function getRoleBindingEndpoint (name, transaction) { + const binding = await RbacRoleBindingManager.getRoleBinding(name, transaction) + if (!binding) { + throw new Errors.NotFoundError(`RoleBinding '${name}' not found`) + } + return { + binding: binding + } +} + +async function createRoleBindingEndpoint (bindingData, transaction) { + // Validate schema + await Validator.validate(bindingData, Validator.schemas.roleBindingCreate) + + const binding = await RbacRoleBindingManager.createRoleBinding(bindingData, transaction) + return { + binding: binding + } +} + +async function updateRoleBindingEndpoint (name, bindingData, transaction) { + // Validate schema + await Validator.validate(bindingData, Validator.schemas.roleBindingUpdate) + + const binding = await RbacRoleBindingManager.updateRoleBinding(name, bindingData, transaction) + return { + binding: binding + } +} + +async function deleteRoleBindingEndpoint (name, transaction) { + await RbacRoleBindingManager.deleteRoleBinding(name, transaction) + return { + message: `RoleBinding '${name}' deleted successfully` + } +} + +// ServiceAccount Management +async function listServiceAccountsEndpoint (transaction) { + const serviceAccounts = await RbacServiceAccountManager.listServiceAccounts(transaction) + return { + serviceAccounts: serviceAccounts + } +} + +async function getServiceAccountEndpoint (name, transaction) { + const sa = await RbacServiceAccountManager.getServiceAccount(name, transaction) + if (!sa) { + throw new Errors.NotFoundError(`ServiceAccount '${name}' not found`) + } + return { + serviceAccount: sa + } +} + +async function createServiceAccountEndpoint (saData, transaction) { + // Validate schema + await Validator.validate(saData, Validator.schemas.serviceAccountCreate) + + const sa = await RbacServiceAccountManager.createServiceAccount(saData, transaction) + return { + serviceAccount: sa + } +} + +async function updateServiceAccountEndpoint (name, saData, transaction) { + // Validate schema + await Validator.validate(saData, Validator.schemas.serviceAccountUpdate) + + const sa = await RbacServiceAccountManager.updateServiceAccount(name, saData, transaction) + + // Notify linked microservices about the service account update + await _notifyMicroservicesForServiceAccountUpdate(sa.id, transaction) + + return { + serviceAccount: sa + } +} + +async function deleteServiceAccountEndpoint (name, transaction) { + // Get service account first to check if it exists and get its ID + const sa = await RbacServiceAccountManager.getServiceAccount(name, transaction) + if (!sa) { + throw new Errors.NotFoundError(`ServiceAccount '${name}' not found`) + } + + // Check if any microservice is referencing this service account + const referencingMicroservice = await MicroserviceManager.findOne({ + serviceAccountId: sa.id + }, transaction) + + if (referencingMicroservice) { + throw new Errors.ConflictError( + `Cannot delete ServiceAccount '${name}' because it is referenced by microservice '${referencingMicroservice.name}' (uuid: ${referencingMicroservice.uuid}). Please delete or update the microservice first.` + ) + } + + await RbacServiceAccountManager.deleteServiceAccount(name, transaction) + return { + message: `ServiceAccount '${name}' deleted successfully` + } +} + +module.exports = { + // Role endpoints + listRolesEndpoint: TransactionDecorator.generateTransaction(listRolesEndpoint), + getRoleEndpoint: TransactionDecorator.generateTransaction(getRoleEndpoint), + createRoleEndpoint: TransactionDecorator.generateTransaction(createRoleEndpoint), + updateRoleEndpoint: TransactionDecorator.generateTransaction(updateRoleEndpoint), + deleteRoleEndpoint: TransactionDecorator.generateTransaction(deleteRoleEndpoint), + // RoleBinding endpoints + listRoleBindingsEndpoint: TransactionDecorator.generateTransaction(listRoleBindingsEndpoint), + getRoleBindingEndpoint: TransactionDecorator.generateTransaction(getRoleBindingEndpoint), + createRoleBindingEndpoint: TransactionDecorator.generateTransaction(createRoleBindingEndpoint), + updateRoleBindingEndpoint: TransactionDecorator.generateTransaction(updateRoleBindingEndpoint), + deleteRoleBindingEndpoint, + // ServiceAccount endpoints + listServiceAccountsEndpoint: TransactionDecorator.generateTransaction(listServiceAccountsEndpoint), + getServiceAccountEndpoint: TransactionDecorator.generateTransaction(getServiceAccountEndpoint), + createServiceAccountEndpoint: TransactionDecorator.generateTransaction(createServiceAccountEndpoint), + updateServiceAccountEndpoint: TransactionDecorator.generateTransaction(updateServiceAccountEndpoint), + deleteServiceAccountEndpoint: TransactionDecorator.generateTransaction(deleteServiceAccountEndpoint) +} diff --git a/src/services/registry-service.js b/src/services/registry-service.js index 62955855..7df9a2ab 100644 --- a/src/services/registry-service.js +++ b/src/services/registry-service.js @@ -12,6 +12,7 @@ */ const RegistryManager = require('../data/managers/registry-manager') +const SecretHelper = require('../helpers/secret-helper') const Validator = require('../schemas') const Errors = require('../helpers/errors') const ErrorMessages = require('../helpers/error-messages') @@ -23,6 +24,10 @@ const MicroserviceManager = require('../data/managers/microservice-manager') // const Op = Sequelize.Op const AppHelper = require('../helpers/app-helper') +function isPasswordEmpty (password) { + return password == null || (typeof password === 'string' && password.trim() === '') +} + const createRegistry = async function (registry, transaction) { await Validator.validate(registry, Validator.schemas.registryCreate) @@ -38,6 +43,19 @@ const createRegistry = async function (registry, transaction) { const createdRegistry = await RegistryManager.create(registryCreate, transaction) + if (!isPasswordEmpty(registryCreate.password)) { + const encryptedPassword = await SecretHelper.encryptSecret( + { value: registryCreate.password }, + 'registry-' + createdRegistry.id, + 'registry' + ) + await RegistryManager.update( + { id: createdRegistry.id }, + { password: encryptedPassword }, + transaction + ) + } + await _updateChangeTracking(transaction) return { @@ -104,6 +122,10 @@ const updateRegistry = async function (registry, registryId, isCLI, transaction) registryUpdate = AppHelper.deleteUndefinedFields(registryUpdate) + if (registryUpdate.password !== undefined && isPasswordEmpty(registryUpdate.password) && SecretHelper.isVaultReference(existingRegistry.password)) { + await SecretHelper.deleteSecret('registry-' + existingRegistry.id, 'registry') + } + const where = isCLI ? { id: registryId @@ -124,6 +146,15 @@ const updateRegistry = async function (registry, registryId, isCLI, transaction) await _updateChangeTracking(transaction) } +const getRegistry = async function (registryId, isCLI, transaction) { + const id = parseInt(registryId, 10) + const registry = await RegistryManager.findOne({ id }, transaction) + if (!registry) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_REGISTRY_ID, registryId)) + } + return registry +} + const _updateChangeTracking = async function (transaction) { const fogs = await FogManager.findAll({}, transaction) for (const fog of fogs) { @@ -135,5 +166,6 @@ module.exports = { createRegistry: TransactionDecorator.generateTransaction(createRegistry), findRegistries: TransactionDecorator.generateTransaction(findRegistries), deleteRegistry: TransactionDecorator.generateTransaction(deleteRegistry), - updateRegistry: TransactionDecorator.generateTransaction(updateRegistry) + updateRegistry: TransactionDecorator.generateTransaction(updateRegistry), + getRegistry: TransactionDecorator.generateTransaction(getRegistry) } diff --git a/src/services/router-service.js b/src/services/router-service.js index 5daada35..f7b7dd8c 100644 --- a/src/services/router-service.js +++ b/src/services/router-service.js @@ -33,9 +33,17 @@ const MicroserviceEnvManager = require('../data/managers/microservice-env-manage const SecretManager = require('../data/managers/secret-manager') const FogManager = require('../data/managers/iofog-manager') const config = require('../config') +const VolumeMountService = require('./volume-mount-service') +const VolumeMappingManager = require('../data/managers/volume-mapping-manager') const SITE_CONFIG_VERSION = 'pot' const SITE_CONFIG_NAMESPACE = process.env.CONTROLLER_NAMESPACE || config.get('app.namespace') +const SSL_PROFILE_PATH = '/etc/skupper-router-certs' +const SYSTEM_DEFAULT_CA_PATH = '/etc/pki/tls/certs/ca-bundle.crt' + +function _sslProfileCertPath (profileName, filename) { + return `${SSL_PROFILE_PATH}/${profileName}/${filename}` +} async function validateAndReturnUpstreamRouters (upstreamRouterIds, isSystemFog, defaultRouter, transaction) { if (!upstreamRouterIds) { @@ -115,6 +123,7 @@ async function createRouterForFog (fogData, uuid, upstreamRouters, transaction) await _createRouterPorts(routerMicroservice.uuid, fogData.edgeRouterPort, transaction) await _createRouterPorts(routerMicroservice.uuid, fogData.interRouterPort, transaction) } + await _ensureRouterSslVolumeMountsAndMappings(uuid, routerMicroservice.uuid, transaction, false) return router } @@ -241,6 +250,9 @@ async function updateConfig (routerID, containerEngine, transaction) { newConfig.connectors[connectorConfig.name] = connectorConfig } + await _ensureRouterSslVolumeMountsAndMappings(router.iofogUuid, routerMicroservice.uuid, transaction, true) + await ChangeTrackingService.update(router.iofogUuid, ChangeTrackingService.events.microserviceConfig, transaction) + // Check if configuration needs update if (JSON.stringify(currentConfig) !== JSON.stringify(newConfig)) { await MicroserviceManager.update( @@ -339,7 +351,11 @@ async function _createRouterMicroservice (isEdge, uuid, microserviceConfig, tran env: [ { key: 'QDROUTERD_CONF', - value: '/home/runner/skupper-router-certs/skrouterd.json' + value: '/tmp/skrouterd.json' + }, + { + key: 'SSL_PROFILE_PATH', + value: SSL_PROFILE_PATH }, { key: 'QDROUTERD_CONF_TYPE', @@ -348,6 +364,10 @@ async function _createRouterMicroservice (isEdge, uuid, microserviceConfig, tran { key: 'SKUPPER_SITE_ID', value: uuid + }, + { + key: 'SKUPPER_PLATFORM', + value: 'pot' } ] } @@ -433,39 +453,36 @@ async function _getRouterMicroserviceConfig (isEdge, uuid, messagingPort, interR sslProfiles: {} } - // Get SSL secrets for all profiles + // Add system-default SSL profile (CA bundle path on host) + config.sslProfiles['system-default'] = { + name: 'system-default', + caCertFile: SYSTEM_DEFAULT_CA_PATH + } + + // Get SSL secrets for all profiles and add path-based sslProfiles (no base64) const siteServerSecret = await SecretManager.getSecret(`${uuid}-site-server`, transaction) const localServerSecret = await SecretManager.getSecret(`${uuid}-local-server`, transaction) const localAgentSecret = await SecretManager.getSecret(`${uuid}-local-agent`, transaction) - // Add SSL profiles - if (siteServerSecret) { - config.sslProfiles[`${uuid}-site-server`] = { - caCert: siteServerSecret.data['ca.crt'], - tlsCert: siteServerSecret.data['tls.crt'], - tlsKey: siteServerSecret.data['tls.key'], - name: `${uuid}-site-server` + function addSslProfileFromSecret (profileName, secret) { + if (!secret) return + const profile = { name: profileName } + if (secret.data && secret.data['ca.crt']) { + profile.caCertFile = _sslProfileCertPath(secret.name, 'ca.crt') } - } - - if (localServerSecret) { - config.sslProfiles[`${uuid}-local-server`] = { - caCert: localServerSecret.data['ca.crt'], - tlsCert: localServerSecret.data['tls.crt'], - tlsKey: localServerSecret.data['tls.key'], - name: `${uuid}-local-server` + if (secret.data && secret.data['tls.crt']) { + profile.certFile = _sslProfileCertPath(profileName, 'tls.crt') } - } - - if (localAgentSecret) { - config.sslProfiles[`${uuid}-local-agent`] = { - caCert: localAgentSecret.data['ca.crt'], - tlsCert: localAgentSecret.data['tls.crt'], - tlsKey: localAgentSecret.data['tls.key'], - name: `${uuid}-local-agent` + if (secret.data && secret.data['tls.key']) { + profile.privateKeyFile = _sslProfileCertPath(profileName, 'tls.key') } + config.sslProfiles[profileName] = profile } + addSslProfileFromSecret(`${uuid}-site-server`, siteServerSecret) + addSslProfileFromSecret(`${uuid}-local-server`, localServerSecret) + addSslProfileFromSecret(`${uuid}-local-agent`, localAgentSecret) + // Add default AMQP listener (internal) config.listeners[`${uuid}-amqp`] = { host: '0.0.0.0', @@ -515,6 +532,70 @@ async function _getRouterMicroserviceConfig (isEdge, uuid, messagingPort, interR return config } +const ROUTER_SSL_PROFILE_NAMES = (uuid) => [ + `${uuid}-site-server`, + `${uuid}-local-server`, + `${uuid}-local-agent` +] + +async function _ensureRouterSslVolumeMountsAndMappings (iofogUuid, routerMicroserviceUuid, transaction, doCleanup = false) { + const profileNames = ROUTER_SSL_PROFILE_NAMES(iofogUuid) + const profileNamesWithSecret = new Set() + + for (const name of profileNames) { + const secret = await SecretManager.getSecret(name, transaction) + if (!secret) continue + + profileNamesWithSecret.add(name) + + // Volume mount: get or create, then link to fog if not already linked + try { + await VolumeMountService.getVolumeMountEndpoint(name, transaction) + } catch (err) { + if (err.name !== 'NotFoundError') throw err + await VolumeMountService.createVolumeMountEndpoint({ name, secretName: name }, transaction) + } + const linkedFogUuids = await VolumeMountService.findVolumeMountedFogNodes(name, transaction) + if (!linkedFogUuids.includes(iofogUuid)) { + await VolumeMountService.linkVolumeMountEndpoint(name, [iofogUuid], transaction) + } + + // Volume mapping: create if not exists + const containerDest = `${SSL_PROFILE_PATH}/${name}` + const existingMapping = await VolumeMappingManager.findOne({ + microserviceUuid: routerMicroserviceUuid, + hostDestination: name, + containerDestination: containerDest, + type: 'volumeMount' + }, transaction) + if (!existingMapping) { + await VolumeMappingManager.create({ + microserviceUuid: routerMicroserviceUuid, + hostDestination: name, + containerDestination: containerDest, + accessMode: 'ro', + type: 'volumeMount' + }, transaction) + } + } + + // Cleanup: remove router SSL volume mappings whose profile no longer has a secret + if (doCleanup) { + const currentMappings = await VolumeMappingManager.findAll( + { microserviceUuid: routerMicroserviceUuid }, + transaction + ) + for (const mapping of currentMappings) { + const isRouterSsl = mapping.type === 'volumeMount' && + mapping.containerDestination && + mapping.containerDestination.startsWith(SSL_PROFILE_PATH) + if (isRouterSsl && mapping.hostDestination && !profileNamesWithSecret.has(mapping.hostDestination)) { + await VolumeMappingManager.delete({ id: mapping.id }, transaction) + } + } + } +} + async function getNetworkRouter (networkRouterId, transaction) { const query = {} if (!networkRouterId) { diff --git a/src/services/services-service.js b/src/services/services-service.js index 2b2c0b59..6f2dc728 100644 --- a/src/services/services-service.js +++ b/src/services/services-service.js @@ -53,45 +53,57 @@ async function _setTags (serviceModel, tagsArray, transaction) { } async function handleServiceDistribution (serviceTags, transaction) { + const tags = Array.isArray(serviceTags) ? serviceTags : (serviceTags ? [].concat(serviceTags) : []) + logger.debug('handleServiceDistribution: entry', { serviceTagsType: typeof serviceTags, isArray: Array.isArray(serviceTags), tagsLength: tags.length }) + // Always find fog nodes with 'all' tag - const allTaggedFogNodes = await FogManager.findAllWithTags({ + const allTaggedFogNodesRaw = await FogManager.findAllWithTags({ '$tags.value$': `${SERVICE_ANNOTATION_TAG}: all` }, transaction) + const allTaggedFogNodes = Array.isArray(allTaggedFogNodesRaw) ? allTaggedFogNodesRaw : [] + logger.debug('handleServiceDistribution: allTaggedFogNodes', { length: allTaggedFogNodes.length }) // If serviceTags is null or empty, return only fog nodes with 'all' tag - if (!serviceTags || serviceTags.length === 0) { - const uuids = allTaggedFogNodes.map(fog => fog.uuid) + if (!tags.length) { + const uuids = Array.from(allTaggedFogNodes).map(fog => fog.uuid) + logger.debug('handleServiceDistribution: early return (no tags)', { uuidsCount: uuids.length }) return uuids } // Filter tags that don't contain ':' or '=' - const filteredServiceTags = serviceTags + const filteredServiceTags = tags .filter(tag => tag != null) .map(tag => String(tag)) .filter(tag => !tag.includes(':') && !tag.includes('=')) .filter(tag => tag.length > 0) if (filteredServiceTags.length === 0) { - const uuids = allTaggedFogNodes.map(fog => fog.uuid) + const uuids = Array.from(allTaggedFogNodes).map(fog => fog.uuid) + logger.debug('handleServiceDistribution: early return (filtered empty)', { uuidsCount: uuids.length }) return uuids } // Find fog nodes for each filtered tag const specificTaggedFogNodes = new Set() for (const tag of filteredServiceTags) { - const fogNodes = await FogManager.findAllWithTags({ + const fogNodesRaw = await FogManager.findAllWithTags({ '$tags.value$': `${SERVICE_ANNOTATION_TAG}: ${tag}` }, transaction) + const fogNodes = Array.isArray(fogNodesRaw) ? fogNodesRaw : [] fogNodes.forEach(fog => specificTaggedFogNodes.add(fog.uuid)) } - // Get all tag fog node UUIDs - const allTagUuids = allTaggedFogNodes.map(fog => fog.uuid) + // Get all tag fog node UUIDs (force real array for spread) + const allTagUuids = Array.from(allTaggedFogNodes).map(fog => fog.uuid) + const allTagUuidsArr = Array.from(allTagUuids) + const specificArr = Array.from(specificTaggedFogNodes) + logger.debug('handleServiceDistribution: before Set', { allTagUuidsLength: allTagUuidsArr.length, specificSize: specificArr.length }) // Combine both sets of fog nodes and remove duplicates - const allFogUuids = new Set([...allTagUuids, ...Array.from(specificTaggedFogNodes)]) - - return Array.from(allFogUuids) + const allFogUuids = new Set([...allTagUuidsArr, ...specificArr]) + const result = Array.from(allFogUuids) + logger.debug('handleServiceDistribution: exit', { resultCount: result.length }) + return result } async function checkKubernetesEnvironment () { @@ -262,7 +274,7 @@ async function _determineConnectorHost (serviceConfig, transaction) { } else { return `iofog_${serviceConfig.resource}` } - case 'agent': + case 'agent': // TODO: find agent extract router config mode from agent router mode. return 'iofog' case 'k8s': case 'external': @@ -650,6 +662,7 @@ async function _updateTcpConnector (serviceConfig, transaction) { // Helper function to delete tcpConnector from router config async function _deleteTcpConnector (serviceName, transaction) { + logger.debug('_deleteTcpConnector: start', { serviceName }) const isK8s = await checkKubernetesEnvironment() const connectorName = `${serviceName}-connector` @@ -658,6 +671,7 @@ async function _deleteTcpConnector (serviceName, transaction) { if (!service) { throw new Errors.NotFoundError(`Service not found: ${serviceName}`) } + logger.debug('_deleteTcpConnector: service', { type: service.type, resource: service.resource, defaultBridge: service.defaultBridge }) const isDefaultRouter = service.defaultBridge === 'default-router' let microserviceSource = null @@ -673,6 +687,7 @@ async function _deleteTcpConnector (serviceName, transaction) { } if (isDefaultRouter && (!microserviceSource || !fogSource)) { + logger.debug('_deleteTcpConnector: updating default router config') if (isK8s) { // Update K8s router config const configMap = await K8sClient.getConfigMap(K8S_ROUTER_CONFIG_MAP) @@ -703,6 +718,8 @@ async function _deleteTcpConnector (serviceName, transaction) { await _updateRouterMicroserviceConfig(fogNodeUuid, currentConfig, transaction) } + logger.debug('_deleteTcpConnector: done (default router updated)') + return } let fogNodeUuid = null @@ -715,6 +732,7 @@ async function _deleteTcpConnector (serviceName, transaction) { if (fogSource) { fogNodeUuid = fogSource.uuid } + logger.debug('_deleteTcpConnector: fogNodeUuid for non-default', { fogNodeUuid }) const routerMicroservice = await _getRouterMicroservice(fogNodeUuid, transaction) const currentConfig = JSON.parse(routerMicroservice.config || '{}') @@ -722,11 +740,14 @@ async function _deleteTcpConnector (serviceName, transaction) { delete currentConfig.bridges.tcpConnectors[connectorName] } + logger.debug('_deleteTcpConnector: updating router config', { fogNodeUuid }) await _updateRouterMicroserviceConfig(fogNodeUuid, currentConfig, transaction) + logger.debug('_deleteTcpConnector: done') } // Helper function to delete tcpListener from router config async function _deleteTcpListener (serviceName, transaction) { + logger.debug('_deleteTcpListener: start', { serviceName }) const isK8s = await checkKubernetesEnvironment() const listenerName = `${serviceName}-listener` @@ -751,6 +772,7 @@ async function _deleteTcpListener (serviceName, transaction) { if (!service) { throw new Errors.NotFoundError(`Service not found: ${serviceName}`) } + logger.debug('_deleteTcpListener: service', { type: service.type, hasTags: !!service.tags, tagsIsArray: Array.isArray(service.tags) }) let microserviceSource = null if (service.type === 'microservice') { @@ -758,8 +780,10 @@ async function _deleteTcpListener (serviceName, transaction) { } // Handle distributed router microservice cases // Get list of fog nodes that need this listener removed - const serviceTags = service.tags.map(tag => tag.value) + const serviceTags = (service.tags && Array.isArray(service.tags)) ? service.tags.map(tag => tag.value) : [] + logger.debug('_deleteTcpListener: calling handleServiceDistribution', { serviceTagsLength: serviceTags.length, serviceTagsSample: serviceTags.slice(0, 3) }) const fogNodeUuids = await handleServiceDistribution(serviceTags, transaction) + logger.debug('_deleteTcpListener: handleServiceDistribution returned', { fogNodeUuidsLength: fogNodeUuids ? fogNodeUuids.length : 'null/undefined', isArray: Array.isArray(fogNodeUuids) }) if (microserviceSource) { if (!fogNodeUuids.includes(microserviceSource.iofogUuid)) { @@ -787,7 +811,9 @@ async function _deleteTcpListener (serviceName, transaction) { // } // Remove listener from each router microservice - for (const fogNodeUuid of fogNodeUuids) { + const fogList = Array.isArray(fogNodeUuids) ? fogNodeUuids : [] + logger.debug('_deleteTcpListener: iterating router configs', { count: fogList.length }) + for (const fogNodeUuid of fogList) { try { const routerMicroservice = await _getRouterMicroservice(fogNodeUuid, transaction) const currentConfig = JSON.parse(routerMicroservice.config || '{}') @@ -797,12 +823,25 @@ async function _deleteTcpListener (serviceName, transaction) { await _updateRouterMicroserviceConfig(fogNodeUuid, currentConfig, transaction) } catch (err) { if (err instanceof Errors.NotFoundError) { - logger.info(`Router microservice not found for fogNodeUuid ${fogNodeUuid}, skipping.`) + logger.info('_deleteTcpListener: router microservice not found, skipping', { fogNodeUuid }) continue } + logger.error('_deleteTcpListener: error updating router config', { fogNodeUuid, message: err.message }) throw err } } + logger.debug('_deleteTcpListener: done') +} + +// Common labels for Kubernetes services created by the controller +function _getK8sServiceLabels () { + return { + 'app.kubernetes.io/name': 'pot', + 'app.kubernetes.io/component': 'controller', + 'app.kubernetes.io/managed-by': 'controller', + 'datasance.com/component': 'router', + 'app.kubernetes.io/instance': process.env.CONTROLLER_NAME || config.get('app.name') + } } // Helper function to create Kubernetes service @@ -813,6 +852,7 @@ async function _createK8sService (serviceConfig, transaction) { kind: 'Service', metadata: { name: serviceConfig.name, + labels: _getK8sServiceLabels(), annotations: normalizedTags.reduce((acc, tag) => { const [key, value] = tag.split(':') acc[key] = (value || '').trim() @@ -861,6 +901,7 @@ async function _updateK8sService (serviceConfig, transaction) { const normalizedTags = serviceConfig.tags.map(tag => tag.includes(':') ? tag : `${tag}:`) const patchData = { metadata: { + labels: _getK8sServiceLabels(), annotations: normalizedTags.reduce((acc, tag) => { const [key, value] = tag.split(':') acc[key] = (value || '').trim() @@ -1138,36 +1179,46 @@ async function updateServiceEndpoint (serviceName, serviceData, transaction) { // Delete service endpoint async function deleteServiceEndpoint (serviceName, transaction) { + logger.debug('deleteServiceEndpoint: start', { serviceName }) // Get existing service const existingService = await ServiceManager.findOne({ name: serviceName }, transaction) if (!existingService) { throw new Errors.NotFoundError(`Service with name ${serviceName} not found`) } + logger.debug('deleteServiceEndpoint: existingService', { type: existingService.type, defaultBridge: existingService.defaultBridge }) const isK8s = await checkKubernetesEnvironment() try { // Delete TCP connector + logger.debug('deleteServiceEndpoint: deleting TCP connector') await _deleteTcpConnector(serviceName, transaction) + logger.debug('deleteServiceEndpoint: TCP connector deleted') // Delete TCP listener + logger.debug('deleteServiceEndpoint: deleting TCP listener') await _deleteTcpListener(serviceName, transaction) + logger.debug('deleteServiceEndpoint: TCP listener deleted') // Delete K8s service if needed if (isK8s && existingService.type !== 'k8s') { + logger.debug('deleteServiceEndpoint: deleting K8s service') await _deleteK8sService(serviceName) + logger.debug('deleteServiceEndpoint: K8s service deleted') } // Finally delete the service from database + logger.debug('deleteServiceEndpoint: deleting service from DB') await ServiceManager.delete({ name: serviceName }, transaction) + logger.debug('deleteServiceEndpoint: done') return { message: `Service ${serviceName} deleted successfully` } } catch (error) { - logger.error('Error deleting service:', { - error: error.message, + logger.error('deleteServiceEndpoint: error', { + serviceName, + message: error.message, stack: error.stack, - serviceName: serviceName, - serviceType: existingService.type + constructorName: error.constructor && error.constructor.name }) // Wrap the error in a proper error type if it's not already diff --git a/src/services/yaml-parser-service.js b/src/services/yaml-parser-service.js index 1fd3de44..a67c2f49 100644 --- a/src/services/yaml-parser-service.js +++ b/src/services/yaml-parser-service.js @@ -363,7 +363,8 @@ const parseMicroserviceYAML = async (microservice) => { pubTags: lget(microservice, 'msRoutes.pubTags', []), subTags: lget(microservice, 'msRoutes.subTags', []), application: microservice.application, - schedule: lget(microservice, 'schedule', 50) + schedule: lget(microservice, 'schedule', 50), + serviceAccount: lget(microservice, 'serviceAccount', undefined) } _deleteUndefinedFields(microserviceData) return microserviceData @@ -404,6 +405,80 @@ async function parseMicroserviceFile (fileContent) { const _deleteUndefinedFields = (obj) => Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]) +async function parseRoleFile (fileContent) { + try { + const doc = yaml.load(fileContent) + if (doc.kind !== 'Role') { + throw new Errors.ValidationError(`Invalid kind ${doc.kind}, expected Role`) + } + if (doc.metadata == null) { + throw new Errors.ValidationError('Invalid YAML format: metadata is required') + } + return { + name: doc.metadata.name, + kind: doc.kind, + // apiVersion removed - not stored in database + // namespace removed - not stored in database (controller manages single namespace) + rules: doc.rules || [] + } + } catch (error) { + if (error instanceof Errors.ValidationError) { + throw error + } + throw new Errors.ValidationError(`Error parsing Role YAML: ${error.message}`) + } +} + +async function parseRoleBindingFile (fileContent) { + try { + const doc = yaml.load(fileContent) + if (doc.kind !== 'RoleBinding') { + throw new Errors.ValidationError(`Invalid kind ${doc.kind}, expected RoleBinding`) + } + if (doc.metadata == null || doc.roleRef == null) { + throw new Errors.ValidationError('Invalid YAML format: metadata and roleRef are required') + } + return { + name: doc.metadata.name, + kind: doc.kind, + // apiVersion removed - not stored in database + // namespace removed - not stored in database (controller manages single namespace) + roleRef: doc.roleRef, + subjects: doc.subjects || [] + } + } catch (error) { + if (error instanceof Errors.ValidationError) { + throw error + } + throw new Errors.ValidationError(`Error parsing RoleBinding YAML: ${error.message}`) + } +} + +async function parseServiceAccountFile (fileContent) { + try { + const doc = yaml.load(fileContent) + if (doc.kind !== 'ServiceAccount') { + throw new Errors.ValidationError(`Invalid kind ${doc.kind}, expected ServiceAccount`) + } + if (doc.metadata == null) { + throw new Errors.ValidationError('Invalid YAML format: metadata is required') + } + if (!doc.roleRef || !doc.roleRef.name) { + throw new Errors.ValidationError('ServiceAccount must have a roleRef with a name') + } + return { + name: doc.metadata.name, + // namespace removed - not stored in database (controller manages single namespace) + roleRef: doc.roleRef + } + } catch (error) { + if (error instanceof Errors.ValidationError) { + throw error + } + throw new Errors.ValidationError(`Error parsing ServiceAccount YAML: ${error.message}`) + } +} + async function parseCertificateFile (fileContent) { try { const doc = yaml.load(fileContent) @@ -443,5 +518,8 @@ module.exports = { parseVolumeMountFile: parseVolumeMountFile, parseConfigMapFile: parseConfigMapFile, parseCertificateFile: parseCertificateFile, - parseServiceFile: parseServiceFile + parseServiceFile: parseServiceFile, + parseRoleFile: parseRoleFile, + parseRoleBindingFile: parseRoleBindingFile, + parseServiceAccountFile: parseServiceAccountFile } diff --git a/src/websocket/server.js b/src/websocket/server.js index 5534d03d..16c49bdd 100644 --- a/src/websocket/server.js +++ b/src/websocket/server.js @@ -10,7 +10,6 @@ const ApplicationManager = require('../data/managers/application-manager') const MicroserviceStatusManager = require('../data/managers/microservice-status-manager') const { microserviceState, microserviceExecState } = require('../enums/microservice-state') const MicroserviceExecStatusManager = require('../data/managers/microservice-exec-status-manager') -const keycloak = require('../config/keycloak.js').initKeycloak() const AuthDecorator = require('../decorators/authorization-decorator') const TransactionDecorator = require('../decorators/transaction-decorator') const msgpack = require('@msgpack/msgpack') @@ -149,11 +148,8 @@ class WebSocketServer { // Handle individual connection errors this.wss.on('connection', (ws, req) => { - logger.info('New WebSocket connection established:' + JSON.stringify({ - url: req.url, - headers: req.headers, - remoteAddress: req.socket.remoteAddress - })) + // Note: Connection logging moved to after successful authorization in handleConnection + // This ensures we only log connections that pass RBAC checks // Set strict WebSocket options for this connection ws.binaryType = 'arraybuffer' // Force binary type to be arraybuffer @@ -300,70 +296,85 @@ class WebSocketServer { // Wrap the entire connection handling in a transaction TransactionDecorator.generateTransaction(async (transaction) => { try { - // Check for token in Authorization header first (for agent and CLI connections) - let token = req.headers.authorization - - // If no token in header, check query parameters (for React UI connections) - if (!token) { - logger.debug('Missing authentication token in header, checking query parameters') - const url = new URL(req.url, `http://${req.headers.host}`) - token = url.searchParams.get('token') - - // If token is found in query params, format it as Bearer token - if (token) { - token = `Bearer ${token}` - // Store in headers for event creation code - req.headers.authorization = token - } - } - - if (!token) { - logger.error('WebSocket connection failed: Missing authentication token neither in header nor query parameters') - try { - ws.close(1008, 'Missing authentication token') - } catch (error) { - logger.error('Error closing WebSocket:' + error.message) - } + // Check if RBAC authorization has already been performed (from registered route middleware) + if (req._rbacAuthorized) { + // RBAC already passed, route directly to appropriate internal handler + logger.debug(`WebSocket connection already RBAC authorized, routing to internal handler: ${req.url}`) + await this._routeToInternalHandler(ws, req, transaction) return } - // Determine connection type and handle accordingly - if (req.url.startsWith('/api/v3/agent/exec/')) { - const microserviceUuid = this.extractMicroserviceUuid(req.url) - if (!microserviceUuid) { - logger.error('WebSocket connection failed: Invalid endpoint - no UUID found') - try { - ws.close(1008, 'Invalid endpoint') - } catch (error) { - logger.error('Error closing WebSocket:' + error.message) + // STEP 1: Check registered routes first (before internal routing) + if (this.routes && this.routes.size > 0) { + logger.debug(`Checking ${this.routes.size} registered routes for ${req.url}`) + + // Extract URL path (without query params) for prefix matching + const urlPath = req.url.split('?')[0].replace(/\/$/, '') + + // Try to find a matching registered route + for (const [routePath, routeData] of this.routes.entries()) { + // Early exit: check prefix before expensive regex match + const routePrefix = routeData.prefix || this.extractRoutePrefix(routePath) + if (!urlPath.startsWith(routePrefix)) { + // Skip this route - URL doesn't start with route prefix + continue } - return - } - await this.handleAgentConnection(ws, req, token, microserviceUuid, transaction) - } else if (req.url.startsWith('/api/v3/microservices/exec/')) { - const microserviceUuid = this.extractMicroserviceUuid(req.url) - if (!microserviceUuid) { - logger.error('WebSocket connection failed: Invalid endpoint - no UUID found') - try { - ws.close(1008, 'Invalid endpoint') - } catch (error) { - logger.error('Error closing WebSocket:' + error.message) + + // Only do regex match if prefix matches + const matchResult = this.matchRoute(routePath, req.url) + if (matchResult && matchResult.matched) { + // Found matching route - extract params and call middleware + // Middleware includes RBAC protection via protectWebSocket + logger.info(`WebSocket route matched: ${routePath} for ${req.url}`, { + routePath, + url: req.url, + params: matchResult.params, + remoteAddress: req.socket.remoteAddress + }) + + // Set route params in req object for middleware access + req.params = matchResult.params + + // Call the registered middleware (includes RBAC authorization) + // If authorization fails, middleware will close the connection + await routeData.middleware(ws, req) + + // If middleware returns successfully, connection is authorized + logger.info(`WebSocket connection authorized and established: ${req.url}`, { + url: req.url, + remoteAddress: req.socket.remoteAddress, + route: routePath + }) + return // Exit early - route handled } - return } - await this.handleUserConnection(ws, req, token, microserviceUuid, transaction) - } else if (req.url.includes('/logs')) { - // Handle log connections (may not have microserviceUuid - could be fog logs) - await this.handleLogConnection(ws, req, token, transaction) - } else { - logger.error('WebSocket connection failed: Invalid endpoint') + + // No registered route matched - deny connection + logger.warn(`WebSocket connection denied: No registered route found for ${req.url}`, { + url: req.url, + registeredRoutes: Array.from(this.routes.keys()), + registeredRouteCount: this.routes.size + }) try { - ws.close(1008, 'Invalid endpoint') + ws.close(1008, 'Route not registered') } catch (error) { - logger.error('Error closing WebSocket:' + error.message) + logger.error('Error closing WebSocket after route mismatch:', error.message) } return + } else { + logger.warn(`WebSocket connection attempted but no routes are registered: ${req.url}`) } + + // STEP 2: Fallback to internal routing (DISABLED for security - all routes must be registered) + // This fallback is only for agent connections which don't use RBAC + // For user connections, all routes MUST be registered with RBAC protection + logger.warn(`WebSocket connection attempted without registered route (fallback disabled): ${req.url}`) + try { + ws.close(1008, 'Route not registered - all WebSocket routes must be registered with RBAC protection') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return } catch (error) { logger.error('WebSocket connection error:' + JSON.stringify({ error: error.message, @@ -376,12 +387,15 @@ class WebSocketServer { try { if (ws.readyState === ws.OPEN) { ws.close(1008, error.message || 'Internal server error') - await MicroserviceExecStatusManager.update( - { microserviceUuid: this.extractMicroserviceUuid(req.url) }, - { execSessionId: '', status: microserviceExecState.INACTIVE }, - transaction - ) - await MicroserviceManager.update({ uuid: this.extractMicroserviceUuid(req.url) }, { execEnabled: false }, transaction) + const microserviceUuid = this.extractMicroserviceUuid(req.url) + if (microserviceUuid) { + await MicroserviceExecStatusManager.update( + { microserviceUuid: microserviceUuid }, + { execSessionId: '', status: microserviceExecState.INACTIVE }, + transaction + ) + await MicroserviceManager.update({ uuid: microserviceUuid }, { execEnabled: false }, transaction) + } } } catch (closeError) { logger.error('Error closing WebSocket connection:' + JSON.stringify({ @@ -398,6 +412,121 @@ class WebSocketServer { }) } + /** + * Route to appropriate internal handler after RBAC authorization + * This method is called when _rbacAuthorized flag is set (from route middleware) + */ + async _routeToInternalHandler (ws, req, transaction) { + try { + // Extract token from headers (already set by protectWebSocket middleware) + let token = req.headers.authorization + if (!token) { + logger.error('WebSocket internal routing failed: Missing authentication token') + try { + ws.close(1008, 'Missing authentication token') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + + // Determine connection type and route to appropriate handler + // IMPORTANT: Check more specific routes (system) BEFORE general routes + if (req.url.startsWith('/api/v3/agent/exec/')) { + // Agent exec connection (agent routes don't use RBAC, but may come through here) + const microserviceUuid = this.extractMicroserviceUuid(req.url) + if (!microserviceUuid) { + logger.error('WebSocket internal routing failed: Invalid endpoint - no UUID found') + try { + ws.close(1008, 'Invalid endpoint') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + await this.handleAgentConnection(ws, req, token, microserviceUuid, transaction) + } else if (req.url.startsWith('/api/v3/microservices/system/exec/')) { + // System microservice exec - check BEFORE regular microservice exec + const microserviceUuid = req.params.microserviceUuid || this.extractMicroserviceUuid(req.url) + if (!microserviceUuid) { + logger.error('WebSocket internal routing failed: Invalid endpoint - no UUID found') + try { + ws.close(1008, 'Invalid endpoint') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + await this.handleUserConnection(ws, req, token, microserviceUuid, true, transaction) // true = expectSystem + } else if (req.url.startsWith('/api/v3/microservices/exec/')) { + // Regular microservice exec + const microserviceUuid = req.params.microserviceUuid || this.extractMicroserviceUuid(req.url) + if (!microserviceUuid) { + logger.error('WebSocket internal routing failed: Invalid endpoint - no UUID found') + try { + ws.close(1008, 'Invalid endpoint') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + await this.handleUserConnection(ws, req, token, microserviceUuid, false, transaction) // false = not system + } else if (req.url.includes('/logs')) { + // Handle log connections - extract parameters from URL/req.params + const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`) + const pathParts = url.pathname.split('/').filter(p => p) + + let microserviceUuid = req.params.uuid || null + let fogUuid = req.params.uuid || null // For iofog routes, uuid is fogUuid + let expectSystem = false + + // Determine if this is a system microservice log or fog log + if (pathParts.includes('microservices') && pathParts.includes('system')) { + const microserviceIndex = pathParts.indexOf('microservices') + const systemIndex = pathParts.indexOf('system') + if (systemIndex === microserviceIndex + 1) { + microserviceUuid = req.params.uuid || pathParts[systemIndex + 1] + expectSystem = true + fogUuid = null + } + } else if (pathParts.includes('microservices')) { + const microserviceIndex = pathParts.indexOf('microservices') + microserviceUuid = req.params.uuid || pathParts[microserviceIndex + 1] + expectSystem = false + fogUuid = null + } else if (pathParts.includes('iofog')) { + const iofogIndex = pathParts.indexOf('iofog') + fogUuid = req.params.uuid || pathParts[iofogIndex + 1] + microserviceUuid = null + expectSystem = false + } + + await this.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, expectSystem, transaction) + } else { + logger.error('WebSocket internal routing failed: Invalid endpoint') + try { + ws.close(1008, 'Invalid endpoint') + } catch (error) { + logger.error('Error closing WebSocket:', error.message) + } + return + } + } catch (error) { + logger.error('WebSocket internal routing error:' + JSON.stringify({ + error: error.message, + stack: error.stack, + url: req.url + })) + if (ws.readyState === WebSocket.OPEN) { + try { + ws.close(1008, error.message || 'Internal routing error') + } catch (closeError) { + logger.error('Error closing WebSocket:', closeError.message) + } + } + } + } + async handleAgentConnection (ws, req, token, microserviceUuid, transaction) { try { this.ensureSocketPongHandler(ws) @@ -746,10 +875,10 @@ class WebSocketServer { } } - async handleUserConnection (ws, req, token, microserviceUuid, transaction) { + async handleUserConnection (ws, req, token, microserviceUuid, expectSystem, transaction) { try { this.ensureSocketPongHandler(ws) - await this.validateUserConnection(token, microserviceUuid, transaction) + await this.validateUserConnection(token, microserviceUuid, expectSystem, transaction) logger.info('User connection validated successfully for microservice:' + microserviceUuid) // Check if there's already an active session for this microservice @@ -1716,213 +1845,90 @@ class WebSocketServer { } } - async validateUserConnection (token, microserviceUuid, transaction) { + async validateUserConnection (token, microserviceUuid, expectSystem, transaction) { try { - // 1. Authenticate user first (Keycloak) - Direct token verification - let userRoles = [] + // 1. Basic token validation + if (!token || !token.replace) { + throw new Errors.AuthenticationError('Missing or invalid authorization token') + } - // Extract Bearer token const bearerToken = token.replace('Bearer ', '') if (!bearerToken) { throw new Errors.AuthenticationError('Missing or invalid authorization token') } - // Check if we're in development mode (mock Keycloak) - const isDevMode = process.env.SERVER_DEV_MODE || config.get('server.devMode', true) - const hasAuthConfig = this.isAuthConfigured() - - if (!hasAuthConfig && isDevMode) { - // Use mock roles for development - userRoles = ['SRE', 'Developer', 'Viewer'] - logger.debug('Using mock authentication for development mode') - } else { - // Use real Keycloak token verification - try { - // Create a grant from the access token - const grant = await keycloak.grantManager.createGrant({ - access_token: bearerToken - }) - - // Extract roles from the token - get client-specific roles - const clientId = process.env.KC_CLIENT || config.get('auth.client.id') - const resourceAccess = grant.access_token.content.resource_access + // 2. Validate microservice existence and type + await this.validateMicroservice(microserviceUuid, expectSystem, transaction) - if (resourceAccess && resourceAccess[clientId] && resourceAccess[clientId].roles) { - userRoles = resourceAccess[clientId].roles - } else { - // Fallback to realm roles if client roles not found - userRoles = grant.access_token.content.realm_access && grant.access_token.content.realm_access.roles - ? grant.access_token.content.realm_access.roles - : [] - } - - logger.debug('Token verification successful, user roles:' + JSON.stringify(userRoles)) - } catch (keycloakError) { - logger.error('Keycloak token verification failed:' + JSON.stringify({ - error: keycloakError.message, - stack: keycloakError.stack - })) - throw new Errors.AuthenticationError('Invalid or expired token') - } - } - - // Check if user has required roles - const hasRequiredRole = userRoles.some(role => ['SRE', 'Developer'].includes(role)) - if (!hasRequiredRole) { - throw new Errors.AuthenticationError('Insufficient permissions. Required roles: SRE for Node Exec or Developer for Microservice Exec') - } - - // 2. Only now check microservice, application, etc. - const microservice = await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction) - if (!microservice) { - throw new Errors.NotFoundError('Microservice not found') - } - - const application = await ApplicationManager.findOne({ id: microservice.applicationId }, transaction) - if (!application) { - throw new Errors.NotFoundError('Application not found') - } - - const statusArr = await MicroserviceStatusManager.findAllExcludeFields({ microserviceUuid: microserviceUuid }, transaction) + // 3. Check microservice status + const statusArr = await MicroserviceStatusManager.findAllExcludeFields({ + microserviceUuid: microserviceUuid + }, transaction) if (!statusArr || statusArr.length === 0) { throw new Errors.NotFoundError('Microservice status not found') } const status = statusArr[0] - logger.debug('Microservice status check:' + JSON.stringify({ - status: status.status, - expectedStatus: microserviceState.RUNNING, - isEqual: status.status === microserviceState.RUNNING - })) if (status.status !== microserviceState.RUNNING) { throw new Errors.ValidationError('Microservice is not running') } - if (application.isSystem && !userRoles.includes('SRE')) { - throw new Errors.AuthenticationError('Only SRE can access system microservices') - } - // For non-system, SRE or Developer is already checked above - - // Check if microservice exec is enabled - if (!microservice.execEnabled) { - throw new Errors.ValidationError('Microservice exec is not enabled') - } - - const execStatusArr = await MicroserviceExecStatusManager.findAllExcludeFields({ microserviceUuid: microserviceUuid }, transaction) - if (!execStatusArr || execStatusArr.length === 0) { - throw new Errors.NotFoundError('Microservice exec status not found') - } - const execStatus = execStatusArr[0] - // logger.debug('Microservice exec status check:' + JSON.stringify({ - // status: execStatus.status, - // expectedStatus: microserviceExecState.ACTIVE, - // isEqual: execStatus.status === microserviceExecState.ACTIVE - // })) - if (execStatus.status === microserviceExecState.ACTIVE) { - throw new Errors.ValidationError('Microservice already has an active session') - } - - return { success: true } // Just indicate validation passed + // 4. RBAC Authorization is handled at route level, so we just validate resources here + // Validation successful + return { success: true } } catch (error) { - logger.error('User connection validation failed:' + JSON.stringify({ error: error.message, stack: error.stack })) + logger.error('User connection validation failed:', { + error: error.message, + stack: error.stack, + microserviceUuid, + expectSystem + }) throw error } } - async validateUserLogsConnection (token, microserviceUuid, fogUuid, transaction) { - try { - // 1. Authenticate user first (Keycloak) - Direct token verification - let userRoles = [] + /** + * Validate microservice exists and check if it's a system microservice + * @param {string} microserviceUuid - Microservice UUID + * @param {boolean} expectSystem - If true, expects system microservice (app.isSystem === true) + * @param {Object} transaction - Database transaction + * @returns {Promise} Microservice object + */ + async validateMicroservice (microserviceUuid, expectSystem, transaction) { + const microservice = await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction) + if (!microservice) { + throw new Errors.NotFoundError(`Microservice not found: ${microserviceUuid}`) + } - // Extract Bearer token - const bearerToken = token.replace('Bearer ', '') - if (!bearerToken) { - throw new Errors.AuthenticationError('Missing or invalid authorization token') + if (expectSystem !== undefined) { + const application = await ApplicationManager.findOne({ id: microservice.applicationId }, transaction) + if (!application) { + throw new Errors.NotFoundError(`Application not found for microservice: ${microserviceUuid}`) } - // Check if we're in development mode (mock Keycloak) - const isDevMode = process.env.SERVER_DEV_MODE || config.get('server.devMode', true) - const hasAuthConfig = this.isAuthConfigured() - - if (!hasAuthConfig && isDevMode) { - // Use mock roles for development - userRoles = ['SRE', 'Developer', 'Viewer'] - logger.debug('Using mock authentication for development mode') - } else { - // Use real Keycloak token verification - try { - // Create a grant from the access token - const grant = await keycloak.grantManager.createGrant({ - access_token: bearerToken - }) - - // Extract roles from the token - get client-specific roles - const clientId = process.env.KC_CLIENT || config.get('auth.client.id') - const resourceAccess = grant.access_token.content.resource_access - - if (resourceAccess && resourceAccess[clientId] && resourceAccess[clientId].roles) { - userRoles = resourceAccess[clientId].roles - } else { - // Fallback to realm roles if client roles not found - userRoles = grant.access_token.content.realm_access && grant.access_token.content.realm_access.roles - ? grant.access_token.content.realm_access.roles - : [] - } - - logger.debug('Token verification successful, user roles:' + JSON.stringify(userRoles)) - } catch (keycloakError) { - logger.error('Keycloak token verification failed:' + JSON.stringify({ - error: keycloakError.message, - stack: keycloakError.stack - })) - throw new Errors.AuthenticationError('Invalid or expired token') - } + const isSystem = application.isSystem === true + if (expectSystem && !isSystem) { + throw new Errors.NotFoundError(`Microservice ${microserviceUuid} is not found`) } - - // Check if user has required roles (SRE/Developer/Viewer for logs) - const hasRequiredRole = userRoles.some(role => ['SRE', 'Developer', 'Viewer'].includes(role)) - if (!hasRequiredRole) { - throw new Errors.AuthenticationError('Insufficient permissions. Required roles: SRE, Developer, or Viewer for log access') + if (!expectSystem && isSystem) { + throw new Errors.NotFoundError(`Microservice ${microserviceUuid} is not found`) } + } - // 2. Validate microservice or fog - if (microserviceUuid) { - const microservice = await MicroserviceManager.findOne({ uuid: microserviceUuid }, transaction) - if (!microservice) { - throw new Errors.NotFoundError('Microservice not found') - } - - const application = await ApplicationManager.findOne({ id: microservice.applicationId }, transaction) - if (!application) { - throw new Errors.NotFoundError('Application not found') - } - - const statusArr = await MicroserviceStatusManager.findAllExcludeFields({ microserviceUuid: microserviceUuid }, transaction) - if (!statusArr || statusArr.length === 0) { - throw new Errors.NotFoundError('Microservice status not found') - } - const status = statusArr[0] - if (status.status !== microserviceState.RUNNING) { - throw new Errors.ValidationError('Microservice is not running') - } - - if (application.isSystem && !userRoles.includes('SRE')) { - throw new Errors.AuthenticationError('Only SRE can access system microservices') - } - } else if (fogUuid) { - const fog = await FogManager.findOne({ uuid: fogUuid }, transaction) - if (!fog) { - throw new Errors.NotFoundError('Fog not found') - } - // For fog logs, we can allow SRE/Developer/Viewer - no additional status check needed - } else { - throw new Errors.ValidationError('Either microserviceUuid or fogUuid must be provided') - } + return microservice + } - return { success: true } // Just indicate validation passed - } catch (error) { - logger.error('User logs connection validation failed:' + JSON.stringify({ error: error.message, stack: error.stack })) - throw error + /** + * Validate fog node exists + * @param {string} fogUuid - Fog UUID + * @param {Object} transaction - Database transaction + * @returns {Promise} Fog object + */ + async validateFog (fogUuid, transaction) { + const fog = await FogManager.findOne({ uuid: fogUuid }, transaction) + if (!fog) { + throw new Errors.NotFoundError(`Fog node not found: ${fogUuid}`) } + return fog } // Singleton instance @@ -2000,14 +2006,100 @@ class WebSocketServer { return match ? match[1] : null } + /** + * Extract static prefix from route pattern (everything before first :param) + * @param {string} routePattern - Route pattern like /api/v3/agent/exec/:microserviceUuid + * @returns {string} - Static prefix like /api/v3/agent/exec + */ + extractRoutePrefix (routePattern) { + // Find first :param + const paramIndex = routePattern.indexOf(':') + if (paramIndex === -1) { + // No params, entire route is prefix + return routePattern.split('?')[0] // Remove query params if any + } + // Return everything before first :param + return routePattern.substring(0, paramIndex).replace(/\/$/, '') + } + registerRoute (path, middleware) { // Store the route handler this.routes = this.routes || new Map() - this.routes.set(path, middleware) + + // Cache route prefix for fast filtering + const prefix = this.extractRoutePrefix(path) + this.routes.set(path, { + middleware, + prefix + }) logger.info('Registered WebSocket route: ' + path) } + /** + * Match a route pattern (e.g., /api/v3/iofog/:uuid/logs) against a URL + * @param {string} routePattern - Route pattern with :param placeholders + * @param {string} url - Full URL including query parameters + * @returns {Object|null} - Match result with params or null if no match + */ + matchRoute (routePattern, url) { + try { + // Strip query parameters and hash from URL + let pathToMatch = url + if (pathToMatch.includes('?')) { + pathToMatch = pathToMatch.split('?')[0] + } + if (pathToMatch.includes('#')) { + pathToMatch = pathToMatch.split('#')[0] + } + pathToMatch = pathToMatch.replace(/\/$/, '') + + // Normalize route pattern + const normalizedRoute = routePattern.replace(/\/$/, '') + + // Convert route pattern to regex (replace :param with capture groups) + const routeRegex = new RegExp('^' + normalizedRoute.replace(/:[^/]+/g, '([^/]+)') + '$') + + // Test match + const matches = pathToMatch.match(routeRegex) + if (!matches) { + // Remove debug log - too noisy, prefix check already filtered most non-matches + return null + } + + // Extract parameter names and values + const paramNames = [] + const paramPattern = /:([^/]+)/g + let match + while ((match = paramPattern.exec(normalizedRoute)) !== null) { + paramNames.push(match[1]) + } + + const params = {} + paramNames.forEach((name, index) => { + if (matches[index + 1]) { + params[name] = matches[index + 1] + } + }) + + logger.debug(`Route pattern matched: ${routePattern} -> ${pathToMatch}`, { + routePattern, + url: pathToMatch, + params + }) + + return { params, matched: true } + } catch (error) { + logger.error('Error matching route pattern:' + JSON.stringify({ + error: error.message, + stack: error.stack, + routePattern, + url + })) + return null + } + } + // Helper method for sending messages to agent async sendMessageToAgent (agent, message, execId, microserviceUuid) { try { @@ -2180,16 +2272,27 @@ class WebSocketServer { // User log connection let microserviceUuid = null let fogUuid = null + let expectSystem = false - if (pathParts.includes('microservices')) { + // Check for system microservice logs first (more specific) + if (pathParts.includes('microservices') && pathParts.includes('system')) { + const microserviceIndex = pathParts.indexOf('microservices') + const systemIndex = pathParts.indexOf('system') + // Path: api, v3, microservices, system, uuid, logs + if (systemIndex === microserviceIndex + 1) { + microserviceUuid = pathParts[systemIndex + 1] + expectSystem = true + } + } else if (pathParts.includes('microservices')) { const microserviceIndex = pathParts.indexOf('microservices') microserviceUuid = pathParts[microserviceIndex + 1] + expectSystem = false } else if (pathParts.includes('iofog')) { const iofogIndex = pathParts.indexOf('iofog') fogUuid = pathParts[iofogIndex + 1] } - await this.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, transaction) + await this.handleUserLogsConnection(ws, req, token, microserviceUuid, fogUuid, expectSystem, transaction) } } catch (error) { logger.error('Error in handleLogConnection:', error) @@ -2199,12 +2302,49 @@ class WebSocketServer { } } - async handleUserLogsConnection (ws, req, token, microserviceUuid, fogUuid, transaction) { + async validateUserLogsConnection (token, microserviceUuid, fogUuid, expectSystem, transaction) { + try { + // 1. Basic token validation + if (!token || !token.replace) { + throw new Errors.AuthenticationError('Missing or invalid authorization token') + } + + const bearerToken = token.replace('Bearer ', '') + if (!bearerToken) { + throw new Errors.AuthenticationError('Missing or invalid authorization token') + } + + // 2. Validate resource existence + if (microserviceUuid) { + // Validate microservice and check if it matches expected system type + await this.validateMicroservice(microserviceUuid, expectSystem, transaction) + } + + if (fogUuid) { + await this.validateFog(fogUuid, transaction) + } + + // 3. RBAC Authorization is handled at route level, so we just validate resources here + // Validation successful + return { success: true } + } catch (error) { + logger.error('User logs connection validation failed:', { + error: error.message, + stack: error.stack, + microserviceUuid, + fogUuid, + expectSystem + }) + throw error + } + } + + async handleUserLogsConnection (ws, req, token, microserviceUuid, fogUuid, expectSystem, transaction) { try { this.ensureSocketPongHandler(ws) // 1. Validate user authentication - await this.validateUserLogsConnection(token, microserviceUuid, fogUuid, transaction) + await this.validateUserLogsConnection(token, microserviceUuid, fogUuid, expectSystem, transaction) // 2. Parse tail configuration from query parameters const url = new URL(req.url, `http://${req.headers.host}`) From 4c783ce8df0efcce7881859ed4cb82411f1b3c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emirhan=20Durmu=C5=9F?= Date: Thu, 5 Feb 2026 17:50:00 +0300 Subject: [PATCH 4/4] viewer version upgraded --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 242f75c9..582fc535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@aws-sdk/client-secrets-manager": "^3.981.0", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", - "@datasance/ecn-viewer": "1.2.6", + "@datasance/ecn-viewer": "1.3.0", "@google-cloud/secret-manager": "^6.1.1", "@kubernetes/client-node": "^0.22.3", "@msgpack/msgpack": "^3.1.2", @@ -1318,9 +1318,9 @@ } }, "node_modules/@datasance/ecn-viewer": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@datasance/ecn-viewer/-/ecn-viewer-1.2.6.tgz", - "integrity": "sha512-/NV1ll6Vt97P3Fdb8oNDZLHGNNNoUW8zF+VVB0RFEEZpUBZqq5vWx72RyNoTP6Jdr7NqnZUb6PA/1Z9GTN3fvw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@datasance/ecn-viewer/-/ecn-viewer-1.3.0.tgz", + "integrity": "sha512-tRjy+H7SVRfLyFLU9LhiibUNIMa1vNNdlYOh7OHaka9BYGurKG39klD4uEADGF+CIcK8AKmTnvwBK72fNpfM8g==" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", diff --git a/package.json b/package.json index 82dc52de..e47c6715 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@aws-sdk/client-secrets-manager": "^3.981.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/identity": "^4.13.0", - "@datasance/ecn-viewer": "1.2.6", + "@datasance/ecn-viewer": "1.3.0", "@google-cloud/secret-manager": "^6.1.1", "@kubernetes/client-node": "^0.22.3", "@msgpack/msgpack": "^3.1.2",