diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 783ba11..23d4c08 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -1,626 +1,723 @@ { - "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.1.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.1.1", - "license": "ISC", - "dependencies": { - "@keeper-security/keeperapi": "^18.0.4", - "ts-node": "^10.7.0", - "typescript": "^4.6.3" - }, - "devDependencies": { - "@types/node": "^25.6.0", - "prettier": "^3.8.1" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@keeper-security/keeperapi": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-18.0.5.tgz", - "integrity": "sha512-13P0mDMgaeUsbsQxlUqV1SMX0X9VAQ5aGa2antZDJWifqNqkZbTQEX/5evuZQN/tsTBn30/2DANpsCQyB1uQmw==", - "license": "ISC", - "dependencies": { - "@noble/post-quantum": "^0.5.2", - "asmcrypto.js": "^2.3.2", - "faye-websocket": "^0.11.3", - "form-data": "^4.0.4", - "node-rsa": "^1.0.8" - }, - "engines": { - "node": ">=24.13.1" - } - }, - "node_modules/@noble/curves": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", - "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/post-quantum": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.4.tgz", - "integrity": "sha512-leww0zzIirrvwaYMPI9fj6aRIlA/c6Y0/lifQQ1YOOyHEr0MNH3yYpjXeiVG+tWdPps4XxGclFWX2INPO3Yo5w==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~2.0.0", - "@noble/hashes": "~2.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT" - }, - "node_modules/asmcrypto.js": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-2.3.2.tgz", - "integrity": "sha512-3FgFARf7RupsZETQ1nHnhLUUvpcttcCq1iZCaVAbJZbCZ5VNRrNyvpDyHTOb0KC3llFcsyOT/a99NZcCbeiEsA==", - "license": "MIT" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-rsa": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", - "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", - "license": "MIT", - "dependencies": { - "asn1": "^0.2.4" - } - }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true + "name": "@keeper-security/keeper-sdk-javascript", + "version": "1.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@keeper-security/keeper-sdk-javascript", + "version": "1.1.1", + "license": "ISC", + "dependencies": { + "@keeper-security/keeperapi": "^18.0.4", + "@keeper-security/secrets-manager-core": "^17.5.0", + "protobufjs": "^7.6.5", + "ts-node": "^10.7.0", + "typescript": "^4.6.3" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "prettier": "^3.8.1" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@keeper-security/keeperapi": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-18.0.5.tgz", + "integrity": "sha512-13P0mDMgaeUsbsQxlUqV1SMX0X9VAQ5aGa2antZDJWifqNqkZbTQEX/5evuZQN/tsTBn30/2DANpsCQyB1uQmw==", + "license": "ISC", + "dependencies": { + "@noble/post-quantum": "^0.5.2", + "asmcrypto.js": "^2.3.2", + "faye-websocket": "^0.11.3", + "form-data": "^4.0.4", + "node-rsa": "^1.0.8" + }, + "engines": { + "node": ">=24.13.1" + } + }, + "node_modules/@keeper-security/secrets-manager-core": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/@keeper-security/secrets-manager-core/-/secrets-manager-core-17.5.0.tgz", + "integrity": "sha512-YRgGbNNtgbXolc3+zsvOl7JJMaIdDuD9ugjpu98uPoo3Wi23StfwiAMO8Yqhif5qRNUBJW7r7t43VFIt9XI6KQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.4.tgz", + "integrity": "sha512-leww0zzIirrvwaYMPI9fj6aRIlA/c6Y0/lifQQ1YOOyHEr0MNH3yYpjXeiVG+tWdPps4XxGclFWX2INPO3Yo5w==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.0.0", + "@noble/hashes": "~2.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/asmcrypto.js": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-2.3.2.tgz", + "integrity": "sha512-3FgFARf7RupsZETQ1nHnhLUUvpcttcCq1iZCaVAbJZbCZ5VNRrNyvpDyHTOb0KC3llFcsyOT/a99NZcCbeiEsA==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } } - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT" - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } } - } } diff --git a/KeeperSdk/package.json b/KeeperSdk/package.json index 755f851..9a20fb7 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -22,6 +22,8 @@ }, "dependencies": { "@keeper-security/keeperapi": "^18.0.4", + "@keeper-security/secrets-manager-core": "^17.5.0", + "protobufjs": "^7.6.5", "ts-node": "^10.7.0", "typescript": "^4.6.3" }, diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 33ce69a..49ec380 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -37,6 +37,7 @@ export { AuditReportErrorCode, ActionReportErrorCode, PasswordReportErrorCode, + PamErrorCode, KEEPER_PUBLIC_HOSTS, isBoolean, isString, @@ -652,6 +653,172 @@ export type { NsfResolvedShareRecipient, } from './nestedShareFolders' +export { + PamManager, + GatewayManager, + listGateways, + formatGatewaysTable, + renderGatewaysAsciiTable, + formatGatewaysJson, + formatGatewaysOutput, + createGateway, + formatCreateGatewayOutput, + editGateway, + formatEditGatewayOutput, + removeGateway, + formatRemoveGatewayOutput, + setGatewayMaxInstances, + formatSetGatewayMaxInstancesOutput, + GatewayListFormat, + GatewayStatus, + GatewayConfigInitFormat, + KSM_APP_RECORD_VERSION, + SUPPORTED_KSM_APP_RECORD_VERSIONS, + APP_NOT_ACCESSIBLE_LABEL, + KSM_CLIENT_ID_MESSAGE, + DEFAULT_GATEWAY_TOKEN_EXPIRES_IN_MIN, + MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN, + MIN_GATEWAY_MAX_INSTANCES, + MAX_GATEWAY_MAX_INSTANCES, + EMPTY_GATEWAYS_MESSAGE, + GATEWAY_LIST_DEFAULT_HEADERS, + GATEWAY_LIST_VERBOSE_HEADERS, + getKeeperRouterBaseUrl, + webSafeUidFromBytes, + controllerUidsEqual, + toFiniteNumber, + formatTimestampMs, + parseGatewayVersionString, + getKsmApplicationDisplayInfo, + resolveKsmApplication, + getKeeperRegionAbbreviation, + formatGatewayOneTimeToken, + findEnterpriseGatewayByUidOrName, + requireEnterpriseGatewayByUidOrName, + fetchEnterprisePamControllers, + groupOnlineGatewaysByControllerUid, + isKeeperRouterConnectionError, + ConfigManager, + listPamConfigurations, + formatPamConfigurationsTable, + renderPamConfigurationsAsciiTable, + formatPamConfigurationsJson, + formatPamConfigurationsOutput, + createPamConfiguration, + formatCreatePamConfigurationOutput, + editPamConfiguration, + formatEditPamConfigurationOutput, + removePamConfiguration, + formatRemovePamConfigurationOutput, + PamConfigListFormat, + PAM_CONFIGURATION_RECORD_VERSION, + PAM_CONFIGURATION_RECORD_TYPES, + PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE, + PAM_CONFIG_ENVIRONMENTS, + PAM_RESOURCES_FIELD_TYPE, + FILE_REF_FIELD_TYPE, + SCHEDULE_FIELD_TYPE, + DEFAULT_PAM_CONFIG_SCHEDULE_VALUE, + EMPTY_PAM_CONFIGURATIONS_MESSAGE, + PAM_CONFIG_LIST_DEFAULT_HEADERS, + PAM_CONFIG_LIST_VERBOSE_HEADERS, + PAM_CONFIG_DETAIL_HEADERS, + PAM_CONFIG_DETAIL_LABELS, + PAM_CONFIG_PERMISSION_DAG_KEYS, + PAM_CONFIG_PERMISSION_FLAGS, + PAM_CONFIG_PERMISSION_VALUES, + isPamConfigurationRecordType, + isPamConfigEnvironment, + resolvePamConfigurationRecordType, + isPamConfigurationRecord, + getPamConfigurationFields, + parsePamResources, + resolveSharedFolderName, + findSharedFolderUidForRecord, + listPamConfigurationRecords, + getPamConfigurationDisplayName, + normalizeFields, + ensureScheduleField, + mergeRecordFields, + readTypedRecordPayload, + upsertPamResourcesField, + resolveSharedFolderUid, + resolveGatewayUidSoft, + findPamConfigurationByUidOrTitle, + resolveResourceRecordUidsToRemove, + linkConfigurationController, + moveConfigurationToSharedFolder, + hasPermissionsInput, + convertPermissionValue, + normalizePermissionValue, + buildAllowedSettingsFromPermissions, + applyPamConfigurationPermissions, + isPamConfigurationInFolder, + resolvePamConfigFolder, + findPamConfigFolderForRecord, + resolvePamConfigFolderTargetFromUid, + resolvePamConfigFolderName, + formatPamConfigFolderDisplay, + placePamConfigurationInFolder, +} from './pam' +export type { + ListGatewaysOptions, + GatewayCounts, + GatewayOsMetadata, + GatewayVersionParts, + GatewayPoolInstance, + GatewayListRow, + GatewayConnectivityStatus, + ListGatewaysResult, + FormattedGatewaysTable, + FormatGatewaysTableOptions, + RenderGatewaysAsciiTableOptions, + GatewayListFormatInput, + GatewayConfigInitFormatInput, + KsmApplicationDisplayInfo, + ResolvedKsmApplication, + CreateGatewayInput, + CreateGatewayResult, + EditGatewayInput, + EditGatewayResult, + RemoveGatewayInput, + RemoveGatewayResult, + SetGatewayMaxInstancesInput, + SetGatewayMaxInstancesResult, + GatewayJsonPoolInstance, + GatewayJsonEntry, + GatewaysJsonPayload, + KsmAppRecordVersion, + PamConfigurationRecordType, + PamConfigEnvironment, + PamConfigPermissionFlag, + PamConfigListFormatInput, + ListPamConfigurationsOptions, + PamResourcesInfo, + PamConfigurationField, + PamConfigurationListRow, + PamConfigurationDetail, + ListPamConfigurationsResult, + FormattedPamConfigurationsTable, + FormatPamConfigurationsTableOptions, + RenderPamConfigurationsAsciiTableOptions, + PamConfigurationJsonField, + PamConfigurationJsonEntry, + PamConfigurationsJsonPayload, + PamConfigurationRecordFieldInput, + PamConfigurationPermissionValue, + PamConfigurationPermissionsInput, + PamNetworkAllowedSettings, + CreatePamConfigurationInput, + CreatePamConfigurationResult, + EditPamConfigurationInput, + EditPamConfigurationResult, + RemovePamConfigurationInput, + RemovePamConfigurationResult, + PamConfigFolderKind, + PamConfigFolderTarget, +} from './pam' + export type { DRecord, DRecordMetadata, diff --git a/KeeperSdk/src/pam/PamManager.ts b/KeeperSdk/src/pam/PamManager.ts new file mode 100644 index 0000000..5b6d03c --- /dev/null +++ b/KeeperSdk/src/pam/PamManager.ts @@ -0,0 +1,175 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { ConfigManager } from './config/ConfigManager' +import { GatewayManager } from './gateway/GatewayManager' +import type { + FormatPamConfigurationsTableOptions, + FormattedPamConfigurationsTable, + ListPamConfigurationsOptions, + ListPamConfigurationsResult, + RenderPamConfigurationsAsciiTableOptions, + CreatePamConfigurationInput, + CreatePamConfigurationResult, + EditPamConfigurationInput, + EditPamConfigurationResult, + RemovePamConfigurationInput, + RemovePamConfigurationResult, +} from './config/configTypes' +import type { + CreateGatewayInput, + CreateGatewayResult, + EditGatewayInput, + EditGatewayResult, + FormatGatewaysTableOptions, + FormattedGatewaysTable, + ListGatewaysOptions, + ListGatewaysResult, + RemoveGatewayInput, + RemoveGatewayResult, + RenderGatewaysAsciiTableOptions, + SetGatewayMaxInstancesInput, + SetGatewayMaxInstancesResult, +} from './gateway/gatewayTypes' + +export type AuthProvider = () => Auth + +export class PamManager { + private readonly gatewayManager: GatewayManager + private readonly configManager: ConfigManager + + constructor(storage: InMemoryStorage, authProvider: AuthProvider) { + this.gatewayManager = new GatewayManager(storage, authProvider) + this.configManager = new ConfigManager(storage, authProvider) + } + + public getGatewayManager(): GatewayManager { + return this.gatewayManager + } + + public getConfigManager(): ConfigManager { + return this.configManager + } + + public async listGateways(options: ListGatewaysOptions = {}): Promise { + return this.gatewayManager.listGateways(options) + } + + public async createGateway(input: CreateGatewayInput): Promise { + return this.gatewayManager.createGateway(input) + } + + public formatCreateGatewayOutput(result: CreateGatewayResult): string { + return this.gatewayManager.formatCreateGatewayOutput(result) + } + + public async editGateway(input: EditGatewayInput): Promise { + return this.gatewayManager.editGateway(input) + } + + public formatEditGatewayOutput(result: EditGatewayResult): string { + return this.gatewayManager.formatEditGatewayOutput(result) + } + + public async removeGateway(input: RemoveGatewayInput): Promise { + return this.gatewayManager.removeGateway(input) + } + + public formatRemoveGatewayOutput(result: RemoveGatewayResult): string { + return this.gatewayManager.formatRemoveGatewayOutput(result) + } + + public async setGatewayMaxInstances(input: SetGatewayMaxInstancesInput): Promise { + return this.gatewayManager.setGatewayMaxInstances(input) + } + + public formatSetGatewayMaxInstancesOutput(result: SetGatewayMaxInstancesResult): string { + return this.gatewayManager.formatSetGatewayMaxInstancesOutput(result) + } + + public formatGatewaysTable( + result: ListGatewaysResult, + options: FormatGatewaysTableOptions = {} + ): FormattedGatewaysTable { + return this.gatewayManager.formatGatewaysTable(result, options) + } + + public renderGatewaysAsciiTable( + table: FormattedGatewaysTable, + options: RenderGatewaysAsciiTableOptions = {} + ): string { + return this.gatewayManager.renderGatewaysAsciiTable(table, options) + } + + public formatGatewaysJson(result: ListGatewaysResult, options: ListGatewaysOptions = {}): string { + return this.gatewayManager.formatGatewaysJson(result, options) + } + + public formatGatewaysOutput(result: ListGatewaysResult, options: ListGatewaysOptions = {}): string { + return this.gatewayManager.formatGatewaysOutput(result, options) + } + + public listPamConfigurations(options: ListPamConfigurationsOptions = {}): ListPamConfigurationsResult { + return this.configManager.listPamConfigurations(options) + } + + public async createPamConfiguration(input: CreatePamConfigurationInput & { returnValue: true }): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput & { returnValue?: false } + ): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput + ): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput + ): Promise { + return this.configManager.createPamConfiguration(input) + } + + public formatCreatePamConfigurationOutput(result: CreatePamConfigurationResult): string { + return this.configManager.formatCreatePamConfigurationOutput(result) + } + + public async editPamConfiguration(input: EditPamConfigurationInput): Promise { + return this.configManager.editPamConfiguration(input) + } + + public formatEditPamConfigurationOutput(result: EditPamConfigurationResult): string { + return this.configManager.formatEditPamConfigurationOutput(result) + } + + public async removePamConfiguration(input: RemovePamConfigurationInput): Promise { + return this.configManager.removePamConfiguration(input) + } + + public formatRemovePamConfigurationOutput(result: RemovePamConfigurationResult): string { + return this.configManager.formatRemovePamConfigurationOutput(result) + } + + public formatPamConfigurationsTable( + result: ListPamConfigurationsResult, + options: FormatPamConfigurationsTableOptions = {} + ): FormattedPamConfigurationsTable { + return this.configManager.formatPamConfigurationsTable(result, options) + } + + public renderPamConfigurationsAsciiTable( + table: FormattedPamConfigurationsTable, + options: RenderPamConfigurationsAsciiTableOptions = {} + ): string { + return this.configManager.renderPamConfigurationsAsciiTable(table, options) + } + + public formatPamConfigurationsJson( + result: ListPamConfigurationsResult, + options: ListPamConfigurationsOptions = {} + ): string { + return this.configManager.formatPamConfigurationsJson(result, options) + } + + public formatPamConfigurationsOutput( + result: ListPamConfigurationsResult, + options: ListPamConfigurationsOptions = {} + ): string { + return this.configManager.formatPamConfigurationsOutput(result, options) + } +} diff --git a/KeeperSdk/src/pam/config/ConfigManager.ts b/KeeperSdk/src/pam/config/ConfigManager.ts new file mode 100644 index 0000000..2c8f87c --- /dev/null +++ b/KeeperSdk/src/pam/config/ConfigManager.ts @@ -0,0 +1,111 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { KeeperSdkError, ResultCodes } from '../../utils' +import { createPamConfiguration, formatCreatePamConfigurationOutput } from './createConfig' +import { editPamConfiguration, formatEditPamConfigurationOutput } from './editConfig' +import { removePamConfiguration, formatRemovePamConfigurationOutput } from './removeConfig' +import { + formatPamConfigurationsJson, + formatPamConfigurationsOutput, + formatPamConfigurationsTable, + listPamConfigurations, + renderPamConfigurationsAsciiTable, +} from './listConfigs' +import type { + CreatePamConfigurationInput, + CreatePamConfigurationResult, + EditPamConfigurationInput, + EditPamConfigurationResult, + FormatPamConfigurationsTableOptions, + FormattedPamConfigurationsTable, + ListPamConfigurationsOptions, + ListPamConfigurationsResult, + RemovePamConfigurationInput, + RemovePamConfigurationResult, + RenderPamConfigurationsAsciiTableOptions, +} from './configTypes' + +export type AuthProvider = () => Auth + +export class ConfigManager { + private readonly storage: InMemoryStorage + private readonly authProvider: AuthProvider + + constructor(storage: InMemoryStorage, authProvider: AuthProvider) { + this.storage = storage + this.authProvider = authProvider + } + + private requireAuth(): Auth { + const auth = this.authProvider() + if (!auth?.sessionToken) { + throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN) + } + return auth + } + + public listPamConfigurations(options: ListPamConfigurationsOptions = {}): ListPamConfigurationsResult { + return listPamConfigurations(this.storage, options) + } + + public async createPamConfiguration(input: CreatePamConfigurationInput & { returnValue: true }): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput & { returnValue?: false } + ): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput + ): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput + ): Promise { + return createPamConfiguration(this.requireAuth(), this.storage, input) + } + + public formatCreatePamConfigurationOutput(result: CreatePamConfigurationResult): string { + return formatCreatePamConfigurationOutput(result) + } + + public async editPamConfiguration(input: EditPamConfigurationInput): Promise { + return editPamConfiguration(this.requireAuth(), this.storage, input) + } + + public formatEditPamConfigurationOutput(result: EditPamConfigurationResult): string { + return formatEditPamConfigurationOutput(result) + } + + public async removePamConfiguration(input: RemovePamConfigurationInput): Promise { + return removePamConfiguration(this.requireAuth(), this.storage, input) + } + + public formatRemovePamConfigurationOutput(result: RemovePamConfigurationResult): string { + return formatRemovePamConfigurationOutput(result) + } + + public formatPamConfigurationsTable( + result: ListPamConfigurationsResult, + options: FormatPamConfigurationsTableOptions = {} + ): FormattedPamConfigurationsTable { + return formatPamConfigurationsTable(result, options) + } + + public renderPamConfigurationsAsciiTable( + table: FormattedPamConfigurationsTable, + options: RenderPamConfigurationsAsciiTableOptions = {} + ): string { + return renderPamConfigurationsAsciiTable(table, options) + } + + public formatPamConfigurationsJson( + result: ListPamConfigurationsResult, + options: ListPamConfigurationsOptions = {} + ): string { + return formatPamConfigurationsJson(result, options) + } + + public formatPamConfigurationsOutput( + result: ListPamConfigurationsResult, + options: ListPamConfigurationsOptions = {} + ): string { + return formatPamConfigurationsOutput(result, options) + } +} diff --git a/KeeperSdk/src/pam/config/applyConfigPermissions.ts b/KeeperSdk/src/pam/config/applyConfigPermissions.ts new file mode 100644 index 0000000..d1cfa61 --- /dev/null +++ b/KeeperSdk/src/pam/config/applyConfigPermissions.ts @@ -0,0 +1,123 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { normal64Bytes, pamConfigureNetworkGraphMessage } from '@keeper-security/keeperapi' +import { extractErrorMessage } from '../../utils' +import { + PAM_CONFIG_PERMISSION_DAG_KEYS, + PAM_CONFIG_PERMISSION_FLAGS, + PAM_CONFIG_PERMISSION_VALUES, + type PamConfigPermissionFlag, + type PamNetworkAllowedSettings, +} from './configConstants' +import type { + ApplyPamConfigurationPermissionsOptions, + PamConfigurationPermissionValue, + PamConfigurationPermissionsInput, + PamPermissionBuildResult, +} from './configTypes' + +export function hasPermissionsInput(permissions?: PamConfigurationPermissionsInput | null): boolean { + if (!permissions) return false + return PAM_CONFIG_PERMISSION_FLAGS.some((flag) => { + const value = permissions[flag] + return value != null && String(value).trim() !== '' + }) +} + +export function convertPermissionValue(value: unknown): boolean | null | undefined { + if (value == null) return undefined + if (typeof value === 'boolean') return value + const normalized = String(value).trim().toLowerCase() + if (!normalized) return undefined + if (normalized === 'on' || normalized === 'true' || normalized === '1') return true + if (normalized === 'off' || normalized === 'false' || normalized === '0') return false + if (normalized === 'default') return null + return undefined +} + +export function normalizePermissionValue(value: unknown): PamConfigurationPermissionValue | undefined { + if (value == null) return undefined + const normalized = String(value).trim().toLowerCase() + if ((PAM_CONFIG_PERMISSION_VALUES as readonly string[]).includes(normalized)) { + return normalized as PamConfigurationPermissionValue + } + const converted = convertPermissionValue(value) + if (converted === true) return 'on' + if (converted === false) return 'off' + if (converted === null) return 'default' + return undefined +} + +export function buildAllowedSettingsFromPermissions( + permissions: PamConfigurationPermissionsInput +): PamPermissionBuildResult { + const allowedSettings: PamNetworkAllowedSettings = {} + const applied: Partial> = {} + const defaultResets: PamConfigPermissionFlag[] = [] + const invalid: Array<{ flag: PamConfigPermissionFlag; value: unknown }> = [] + + for (const flag of PAM_CONFIG_PERMISSION_FLAGS) { + const raw = permissions[flag] + if (raw == null || String(raw).trim() === '') continue + + const permissionValue = normalizePermissionValue(raw) + const converted = convertPermissionValue(raw) + if (permissionValue == null || converted === undefined) { + invalid.push({ flag, value: raw }) + continue + } + + applied[flag] = permissionValue + if (converted === null) { + defaultResets.push(flag) + continue + } + + const dagKey = PAM_CONFIG_PERMISSION_DAG_KEYS[flag] + allowedSettings[dagKey] = converted + } + + return { allowedSettings, applied, defaultResets, invalid } +} + +export async function applyPamConfigurationPermissions( + auth: Auth, + configurationUid: string, + permissions: PamConfigurationPermissionsInput, + warnings: string[], + options: ApplyPamConfigurationPermissionsOptions = {} +): Promise { + if (!hasPermissionsInput(permissions)) return false + + const warnOnDefaultReset = options.warnOnDefaultReset !== false + const { allowedSettings, defaultResets, invalid } = buildAllowedSettingsFromPermissions(permissions) + + for (const entry of invalid) { + warnings.push(`Invalid permission value for "${entry.flag}": ${String(entry.value)}. Use on, off, or default.`) + } + + if (warnOnDefaultReset && defaultResets.length > 0) { + warnings.push( + `Permission reset to default is not applied for: ${defaultResets.join(', ')} ` + + `(requires DAG key removal; use on/off, or Commander for default).` + ) + } + + if (Object.keys(allowedSettings).length === 0) return false + + const allowedSettingsBytes = new TextEncoder().encode(JSON.stringify(allowedSettings)) + + try { + await auth.executeRouterRestAction( + pamConfigureNetworkGraphMessage({ + recordUid: normal64Bytes(configurationUid), + networkSettings: { + allowedSettings: allowedSettingsBytes, + }, + }) + ) + return true + } catch (err) { + warnings.push(`Failed to apply configuration permissions: ${extractErrorMessage(err)}`) + return false + } +} diff --git a/KeeperSdk/src/pam/config/configConstants.ts b/KeeperSdk/src/pam/config/configConstants.ts new file mode 100644 index 0000000..ed21751 --- /dev/null +++ b/KeeperSdk/src/pam/config/configConstants.ts @@ -0,0 +1,87 @@ +export const PAM_CONFIGURATION_RECORD_VERSION = 6 + +export const PAM_CONFIGURATION_RECORD_TYPES = [ + 'pamAwsConfiguration', + 'pamAzureConfiguration', + 'pamGcpConfiguration', + 'pamDomainConfiguration', + 'pamNetworkConfiguration', + 'pamOciConfiguration', + 'pamGitHubConfiguration', +] as const + +export type PamConfigurationRecordType = (typeof PAM_CONFIGURATION_RECORD_TYPES)[number] + +export const PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE = { + aws: 'pamAwsConfiguration', + azure: 'pamAzureConfiguration', + gcp: 'pamGcpConfiguration', + domain: 'pamDomainConfiguration', + local: 'pamNetworkConfiguration', + oci: 'pamOciConfiguration', + github: 'pamGitHubConfiguration', +} as const + +export type PamConfigEnvironment = keyof typeof PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE + +export const PAM_CONFIG_ENVIRONMENTS = [ + 'local', + 'aws', + 'azure', + 'gcp', + 'domain', + 'oci', + 'github', +] as const satisfies ReadonlyArray + +export const PAM_RESOURCES_FIELD_TYPE = 'pamResources' as const +export const FILE_REF_FIELD_TYPE = 'fileRef' as const +export const SCHEDULE_FIELD_TYPE = 'schedule' as const + +export const DEFAULT_PAM_CONFIG_SCHEDULE_VALUE = [{ type: 'On-Demand' }] as const + +export const EMPTY_PAM_CONFIGURATIONS_MESSAGE = + 'No PAM Configurations found. Create one with `pam config new` after syncing your vault.' as const + +export const PAM_CONFIG_LIST_DEFAULT_HEADERS = [ + 'UID', + 'Config Name', + 'Config Type', + 'Folder', + 'Gateway UID', + 'Resource Record UIDs', +] as const + +export const PAM_CONFIG_LIST_VERBOSE_HEADERS = ['Fields'] as const + +export const PAM_CONFIG_DETAIL_HEADERS = ['Field', 'Value'] as const + +export const PAM_CONFIG_DETAIL_LABELS = [ + 'UID', + 'Name', + 'Config Type', + 'Folder', + 'Gateway UID', + 'Resource Record UIDs', +] as const + +export const PAM_CONFIG_PERMISSION_DAG_KEYS = { + connections: 'connections', + tunneling: 'portForwards', + rotation: 'rotation', + remoteBrowserIsolation: 'remoteBrowserIsolation', + connectionsRecording: 'sessionRecording', + typescriptRecording: 'typescriptRecording', + aiThreatDetection: 'aiEnabled', + aiTerminateSessionOnDetection: 'aiSessionTerminate', +} as const + +export type PamConfigPermissionFlag = keyof typeof PAM_CONFIG_PERMISSION_DAG_KEYS + +export const PAM_CONFIG_PERMISSION_FLAGS = Object.keys(PAM_CONFIG_PERMISSION_DAG_KEYS) as PamConfigPermissionFlag[] + +export const PAM_CONFIG_PERMISSION_VALUES = ['on', 'off', 'default'] as const + +export type PamNetworkAllowedSettingsKey = (typeof PAM_CONFIG_PERMISSION_DAG_KEYS)[PamConfigPermissionFlag] + +export type PamNetworkAllowedSettings = Partial> diff --git a/KeeperSdk/src/pam/config/configHelpers.ts b/KeeperSdk/src/pam/config/configHelpers.ts new file mode 100644 index 0000000..a203e20 --- /dev/null +++ b/KeeperSdk/src/pam/config/configHelpers.ts @@ -0,0 +1,118 @@ +import type { DRecord, DSharedFolder, DSharedFolderRecord } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { FolderKind, VaultObjectKind, sharedFolderName } from '../../folders/folderHelpers' +import { getRecordFields, getRecordTitle, getRecordType } from '../../records/RecordUtils' +import { + FILE_REF_FIELD_TYPE, + PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE, + PAM_CONFIGURATION_RECORD_TYPES, + PAM_CONFIGURATION_RECORD_VERSION, + PAM_RESOURCES_FIELD_TYPE, + type PamConfigEnvironment, + type PamConfigurationRecordType, +} from './configConstants' +import type { PamConfigurationField, PamResourcesInfo } from './configTypes' + +export function isPamConfigurationRecordType(recordType: string): recordType is PamConfigurationRecordType { + return (PAM_CONFIGURATION_RECORD_TYPES as readonly string[]).includes(recordType) +} + +export function isPamConfigEnvironment(value: string): value is PamConfigEnvironment { + return Object.prototype.hasOwnProperty.call(PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE, value) +} + +export function resolvePamConfigurationRecordType(environmentOrType: string): PamConfigurationRecordType | undefined { + const trimmed = environmentOrType.trim() + if (!trimmed) return undefined + const lowered = trimmed.toLowerCase() + if (isPamConfigEnvironment(lowered)) return PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE[lowered] + if (isPamConfigurationRecordType(trimmed)) return trimmed + return undefined +} + +export function isPamConfigurationRecord(record: DRecord): boolean { + return record.version === PAM_CONFIGURATION_RECORD_VERSION && isPamConfigurationRecordType(getRecordType(record)) +} + +function fieldValueToStrings(value: unknown): string[] { + if (value == null) return [] + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return [String(value)] + } + if (Array.isArray(value)) { + return value.flatMap((entry) => fieldValueToStrings(entry)) + } + if (typeof value === 'object') { + const obj = value as Record + if (typeof obj.value === 'string') return [obj.value] + if (typeof obj.url === 'string') return [obj.url] + return [JSON.stringify(obj)] + } + return [String(value)] +} + +export function getPamConfigurationFields(record: DRecord): PamConfigurationField[] { + return getRecordFields(record) + .filter((field) => field.type !== PAM_RESOURCES_FIELD_TYPE && field.type !== FILE_REF_FIELD_TYPE) + .map((field) => ({ + type: field.type, + label: field.label, + values: (field.value ?? []).flatMap((entry: unknown) => fieldValueToStrings(entry)), + })) +} + +export function parsePamResources(record: DRecord): PamResourcesInfo { + const field = getRecordFields(record).find((entry) => entry.type === PAM_RESOURCES_FIELD_TYPE) + const raw = field?.value?.[0] + if (!raw || typeof raw !== 'object') { + return { gatewayUid: '', sharedFolderUid: '', resourceRecordUids: [] } + } + + const data = raw as { + controllerUid?: unknown + folderUid?: unknown + resourceRef?: unknown + adminCredentialRef?: unknown + } + + const resourceRecordUids = Array.isArray(data.resourceRef) + ? data.resourceRef.map((uid) => String(uid || '')).filter(Boolean) + : typeof data.resourceRef === 'string' && data.resourceRef + ? [data.resourceRef] + : [] + + return { + gatewayUid: data.controllerUid != null ? String(data.controllerUid) : '', + sharedFolderUid: data.folderUid != null ? String(data.folderUid) : '', + resourceRecordUids, + adminCredentialUid: + data.adminCredentialRef != null && String(data.adminCredentialRef).trim() + ? String(data.adminCredentialRef).trim() + : undefined, + } +} + +export function resolveSharedFolderName(storage: InMemoryStorage, sharedFolderUid: string): string { + if (!sharedFolderUid) return '' + const folder = storage.getByUid(FolderKind.SharedFolder, sharedFolderUid) + return folder ? sharedFolderName(folder) : sharedFolderUid +} + +export function findSharedFolderUidForRecord(storage: InMemoryStorage, recordUid: string): string { + const sharedFolderRecord = storage + .getAll(VaultObjectKind.SharedFolderRecord) + .find((candidate) => candidate.recordUid === recordUid) + return sharedFolderRecord?.sharedFolderUid || '' +} + +export function listPamConfigurationRecords(storage: InMemoryStorage): DRecord[] { + return storage + .getRecords() + .filter((record) => record.version === PAM_CONFIGURATION_RECORD_VERSION) + .filter((record) => isPamConfigurationRecordType(getRecordType(record))) +} + +export function getPamConfigurationDisplayName(record: DRecord): string { + const title = getRecordTitle(record) + return title && title !== '(untitled)' && title !== '(no data)' ? title : record.uid +} diff --git a/KeeperSdk/src/pam/config/configMutationHelpers.ts b/KeeperSdk/src/pam/config/configMutationHelpers.ts new file mode 100644 index 0000000..14fb758 --- /dev/null +++ b/KeeperSdk/src/pam/config/configMutationHelpers.ts @@ -0,0 +1,313 @@ +import type { Auth, DRecord } from '@keeper-security/keeperapi' +import { normal64Bytes, setConfigurationControllerMessage } from '@keeper-security/keeperapi' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { moveRecord } from '../../records/RecordOperations' +import { getRecordTitle } from '../../records/RecordUtils' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { + fetchEnterprisePamControllers, + findEnterpriseGatewayByUidOrName, + webSafeUidFromBytes, +} from '../gateway/gatewayHelpers' +import { DEFAULT_PAM_CONFIG_SCHEDULE_VALUE, PAM_RESOURCES_FIELD_TYPE, SCHEDULE_FIELD_TYPE } from './configConstants' +import { + getPamConfigurationDisplayName, + isPamConfigurationRecord, + listPamConfigurationRecords, + parsePamResources, +} from './configHelpers' +import { resolvePamConfigFolder } from './pamConfigFolder' +import type { PamConfigurationRecordFieldInput, PamResourcesInfo } from './configTypes' + +export { getPaddedJsonBytes } from './configRecordPayload' + +export function normalizeFields( + fields: PamConfigurationRecordFieldInput[] | undefined +): PamConfigurationRecordFieldInput[] { + if (!fields?.length) return [] + return fields.map((field) => ({ + type: field.type, + value: Array.isArray(field.value) ? field.value : field.value == null ? [] : [field.value], + label: field.label, + })) +} + +export function ensureScheduleField(fields: PamConfigurationRecordFieldInput[]): PamConfigurationRecordFieldInput[] { + if (fields.some((field) => field.type === SCHEDULE_FIELD_TYPE)) return fields + return [ + ...fields, + { + type: SCHEDULE_FIELD_TYPE, + value: [...DEFAULT_PAM_CONFIG_SCHEDULE_VALUE], + }, + ] +} + +function fieldKey(field: PamConfigurationRecordFieldInput): string { + return `${field.type}\0${field.label || ''}` +} + +export function mergeRecordFields( + existing: PamConfigurationRecordFieldInput[], + updates: PamConfigurationRecordFieldInput[] | undefined +): PamConfigurationRecordFieldInput[] { + if (!updates?.length) return existing.map((field) => ({ ...field, value: [...field.value] })) + + const merged = existing.map((field) => ({ ...field, value: [...field.value] })) + const indexByKey = new Map(merged.map((field, index) => [fieldKey(field), index])) + + for (const update of normalizeFields(updates)) { + const key = fieldKey(update) + const existingIndex = indexByKey.get(key) + if (existingIndex == null) { + indexByKey.set(key, merged.length) + merged.push({ ...update, value: [...update.value] }) + continue + } + merged[existingIndex] = { + ...merged[existingIndex], + ...update, + value: [...update.value], + } + } + return merged +} + +export function readTypedRecordPayload(record: DRecord): { + title: string + configType: string + fields: PamConfigurationRecordFieldInput[] + custom: PamConfigurationRecordFieldInput[] + notes: string +} { + const data = record.data && typeof record.data === 'object' ? record.data : {} + const toInputs = (entries: unknown): PamConfigurationRecordFieldInput[] => { + if (!Array.isArray(entries)) return [] + return entries + .filter((entry) => entry && typeof entry === 'object') + .map((entry) => { + const field = entry as { type?: string; value?: unknown; label?: string } + return { + type: field.type || 'text', + value: Array.isArray(field.value) ? field.value : field.value == null ? [] : [field.value], + label: field.label, + } + }) + } + + return { + title: typeof data.title === 'string' ? data.title : getPamConfigurationDisplayName(record), + configType: typeof data.type === 'string' ? data.type : '', + fields: toInputs(data.fields), + custom: toInputs(data.custom), + notes: typeof data.notes === 'string' ? data.notes : '', + } +} + +export function upsertPamResourcesField( + fields: PamConfigurationRecordFieldInput[], + resources: PamResourcesInfo +): PamConfigurationRecordFieldInput[] { + const pamResources: Record = { + controllerUid: resources.gatewayUid || '', + folderUid: resources.sharedFolderUid || '', + resourceRef: [...resources.resourceRecordUids], + } + if (resources.adminCredentialUid) { + pamResources.adminCredentialRef = resources.adminCredentialUid + } + const without = fields.filter((field) => field.type !== PAM_RESOURCES_FIELD_TYPE) + return [ + { + type: PAM_RESOURCES_FIELD_TYPE, + value: [pamResources], + }, + ...without, + ] +} + +export function resolveSharedFolderUid( + storage: InMemoryStorage, + sharedFolder: string, + options: { required?: boolean } = {} +): string { + return resolvePamConfigFolder(storage, sharedFolder, options).uid +} + +export type ResolveGatewayUidOptions = { + failureResultCode?: string + missingWarning?: string + notFoundWarning?: (gateway: string) => string + resolveFailedWarning?: (gateway: string, error: string) => string +} + +export async function resolveGatewayUidSoft( + auth: Auth, + gatewayUidOrName: string | undefined, + warnings: string[], + options: ResolveGatewayUidOptions = {} +): Promise { + const trimmed = gatewayUidOrName?.trim() || '' + if (!trimmed) { + if (options.missingWarning) warnings.push(options.missingWarning) + return '' + } + + const failureCode = options.failureResultCode || ResultCodes.PAM_CONFIG_CREATE_FAILED + try { + const controllers = await fetchEnterprisePamControllers(auth, failureCode) + const gateway = findEnterpriseGatewayByUidOrName(controllers, trimmed) + const gatewayUid = webSafeUidFromBytes(gateway?.controllerUid) + if (!gatewayUid) { + warnings.push( + options.notFoundWarning?.(trimmed) || + `Gateway "${trimmed}" not found. Continuing without updating the gateway controller link.` + ) + return '' + } + return gatewayUid + } catch (err) { + warnings.push( + options.resolveFailedWarning?.(trimmed, extractErrorMessage(err)) || + `Failed to resolve gateway "${trimmed}": ${extractErrorMessage(err)}. Continuing without updating the gateway controller link.` + ) + return '' + } +} + +export function findPamConfigurationByUidOrTitle(storage: InMemoryStorage, uidOrTitle: string): DRecord { + const trimmed = uidOrTitle.trim() + if (!trimmed) { + throw new KeeperSdkError('PAM Configuration UID or title is required.', ResultCodes.PAM_CONFIG_REQUIRED) + } + + const byUid = storage.getByUid(VaultObjectKind.Record, trimmed) + if (byUid) { + if (!isPamConfigurationRecord(byUid)) { + throw new KeeperSdkError(`Record "${trimmed}" is not a PAM Configuration.`, ResultCodes.PAM_CONFIG_INVALID) + } + return byUid + } + + const lowered = trimmed.toLowerCase() + const matches = listPamConfigurationRecords(storage).filter((record) => { + const title = getPamConfigurationDisplayName(record).toLowerCase() + return title === lowered + }) + + if (matches.length === 0) { + throw new KeeperSdkError(`PAM Configuration "${trimmed}" not found.`, ResultCodes.PAM_CONFIG_NOT_FOUND) + } + if (matches.length > 1) { + throw new KeeperSdkError( + `Multiple PAM Configurations match title "${trimmed}". Use the configuration UID instead.`, + ResultCodes.PAM_MULTIPLE_CONFIG_MATCHES + ) + } + return matches[0] +} + +export function resolveResourceRecordUidsToRemove( + storage: InMemoryStorage, + removeResourceRecords: string[] | undefined, + currentResourceUids: string[], + warnings: string[] +): string[] { + if (!removeResourceRecords?.length) return [] + + const removed: string[] = [] + for (const raw of removeResourceRecords) { + const trimmed = raw.trim() + if (!trimmed) continue + + if (currentResourceUids.includes(trimmed)) { + removed.push(trimmed) + continue + } + + const lowered = trimmed.toLowerCase() + const titleMatches = currentResourceUids.filter((uid) => { + const record = storage.getByUid(VaultObjectKind.Record, uid) + if (!record) return false + return getRecordTitle(record).toLowerCase() === lowered + }) + + if (titleMatches.length === 0) { + warnings.push(`Resource record "${trimmed}" was not found on this configuration and was skipped.`) + continue + } + if (titleMatches.length > 1) { + warnings.push(`Multiple resource records match "${trimmed}"; use a resource UID. Skipped this removal.`) + continue + } + removed.push(titleMatches[0]) + } + + return [...new Set(removed)] +} + +export async function linkConfigurationController( + auth: Auth, + configurationUid: string, + gatewayUid: string +): Promise { + await auth.executeRestAction( + setConfigurationControllerMessage({ + configurationUid: normal64Bytes(configurationUid), + controllerUid: normal64Bytes(gatewayUid), + }) + ) +} + +export async function moveConfigurationToSharedFolder( + auth: Auth, + storage: InMemoryStorage, + configurationUid: string, + sharedFolderUid: string, + options: { srcFolderUid?: string } = {} +): Promise<{ success: boolean; message?: string }> { + try { + const moveResult = await moveRecord(auth, storage, { + recordUid: configurationUid, + dstFolderUid: sharedFolderUid, + srcFolderUid: options.srcFolderUid, + canEdit: true, + }) + return { success: moveResult.success, message: moveResult.message } + } catch (err) { + return { success: false, message: extractErrorMessage(err) } + } +} + +export function getPamResourcesFromFields(fields: PamConfigurationRecordFieldInput[]): PamResourcesInfo { + const field = fields.find((entry) => entry.type === PAM_RESOURCES_FIELD_TYPE) + const raw = field?.value?.[0] + if (!raw || typeof raw !== 'object') { + return { gatewayUid: '', sharedFolderUid: '', resourceRecordUids: [] } + } + const data = raw as { + controllerUid?: unknown + folderUid?: unknown + resourceRef?: unknown + adminCredentialRef?: unknown + } + const resourceRecordUids = Array.isArray(data.resourceRef) + ? data.resourceRef.map((uid) => String(uid || '')).filter(Boolean) + : typeof data.resourceRef === 'string' && data.resourceRef + ? [data.resourceRef] + : [] + return { + gatewayUid: data.controllerUid != null ? String(data.controllerUid) : '', + sharedFolderUid: data.folderUid != null ? String(data.folderUid) : '', + resourceRecordUids, + adminCredentialUid: + data.adminCredentialRef != null && String(data.adminCredentialRef).trim() + ? String(data.adminCredentialRef).trim() + : undefined, + } +} + +export function parsePamResourcesFromRecord(record: DRecord): PamResourcesInfo { + return parsePamResources(record) +} diff --git a/KeeperSdk/src/pam/config/configRecordPayload.ts b/KeeperSdk/src/pam/config/configRecordPayload.ts new file mode 100644 index 0000000..8ef2b9d --- /dev/null +++ b/KeeperSdk/src/pam/config/configRecordPayload.ts @@ -0,0 +1,14 @@ +const MIN_RECORD_PAD_BYTES = 384 +const PAD_BLOCK_SIZE = 16 +const SPACE_BYTE = 0x20 + +export function getPaddedJsonBytes(data: Record): Uint8Array { + const jsonBytes = new TextEncoder().encode(JSON.stringify(data)) + const paddedLength = Math.ceil(Math.max(MIN_RECORD_PAD_BYTES, jsonBytes.length) / PAD_BLOCK_SIZE) * PAD_BLOCK_SIZE + if (jsonBytes.length === paddedLength) return jsonBytes + + const padded = new Uint8Array(paddedLength) + padded.set(jsonBytes) + padded.fill(SPACE_BYTE, jsonBytes.length) + return padded +} diff --git a/KeeperSdk/src/pam/config/configTypes.ts b/KeeperSdk/src/pam/config/configTypes.ts new file mode 100644 index 0000000..e85a4e3 --- /dev/null +++ b/KeeperSdk/src/pam/config/configTypes.ts @@ -0,0 +1,246 @@ +import type { + PamConfigPermissionFlag, + PamNetworkAllowedSettings, + PamNetworkAllowedSettingsKey, +} from './configConstants' + +export type { PamNetworkAllowedSettings, PamNetworkAllowedSettingsKey } + +export enum PamConfigListFormat { + Table = 'table', + Json = 'json', +} + +export type PamConfigListFormatInput = PamConfigListFormat | `${PamConfigListFormat}` + +export type ListPamConfigurationsOptions = { + configUid?: string + verbose?: boolean + format?: PamConfigListFormatInput +} + +export type PamResourcesInfo = { + gatewayUid: string + sharedFolderUid: string + resourceRecordUids: string[] + adminCredentialUid?: string +} + +export type PamConfigurationField = { + type: string + label?: string + values: string[] +} + +export type PamConfigurationListRow = { + uid: string + name: string + configType: string + sharedFolderUid: string + sharedFolderName: string + gatewayUid: string + resourceRecordUids: string[] + fields?: PamConfigurationField[] +} + +export type PamConfigurationDetail = { + uid: string + name: string + configType: string + sharedFolderUid: string + sharedFolderName: string + gatewayUid: string + resourceRecordUids: string[] + fields: PamConfigurationField[] +} + +export type ListPamConfigurationsResult = { + configurations: PamConfigurationListRow[] + detail?: PamConfigurationDetail + warnings: string[] + message?: string +} + +export type FormattedPamConfigurationsTable = { + headers: string[] + rows: string[][] +} + +export type FormatPamConfigurationsTableOptions = { + verbose?: boolean +} + +export type RenderPamConfigurationsAsciiTableOptions = { + minColWidth?: number +} + +export type PamConfigurationJsonField = { + type: string + label?: string + values: string[] +} + +export type PamConfigurationJsonEntry = { + uid: string + name: string + config_type: string + shared_folder_uid: string + shared_folder_name: string + gateway_uid: string + resource_record_uids: string[] + fields?: PamConfigurationJsonField[] +} + +export type PamConfigurationsJsonPayload = { + configurations?: PamConfigurationJsonEntry[] + configuration?: PamConfigurationJsonEntry & { + fields: PamConfigurationJsonField[] + } + warnings?: string[] + message?: string +} + +export type PamConfigurationRecordFieldInput = { + type: string + value: unknown[] + label?: string +} + +export type PamConfigurationPermissionValue = 'on' | 'off' | 'default' + +export type PamConfigurationPermissionsInput = { + connections?: PamConfigurationPermissionValue + tunneling?: PamConfigurationPermissionValue + rotation?: PamConfigurationPermissionValue + remoteBrowserIsolation?: PamConfigurationPermissionValue + connectionsRecording?: PamConfigurationPermissionValue + typescriptRecording?: PamConfigurationPermissionValue + aiThreatDetection?: PamConfigurationPermissionValue + aiTerminateSessionOnDetection?: PamConfigurationPermissionValue +} + +export type PamPermissionBuildResult = { + allowedSettings: PamNetworkAllowedSettings + applied: Partial> + defaultResets: PamConfigPermissionFlag[] + invalid: Array<{ flag: PamConfigPermissionFlag; value: unknown }> +} + +export type ApplyPamConfigurationPermissionsOptions = { + warnOnDefaultReset?: boolean +} + +export type PamConfigFolderKind = 'shared_folder' | 'nsf' + +export type PamConfigFolderTarget = { + kind: PamConfigFolderKind + uid: string +} + +export type PamConfigFolderPlacementResult = { + success: boolean + message?: string +} + +export type PamConfigRecordRemovalResult = { + success: boolean + message?: string +} + +export type PamConfigurationTypedRecordData = { + type: string + title: string + fields: PamConfigurationRecordFieldInput[] + custom: PamConfigurationRecordFieldInput[] + notes: string +} + +export type CreatePamConfigurationInNsfFolderOptions = { + configurationUid: string + configurationUidBytes: Uint8Array + recordKey: Uint8Array + recordPayload: Record + folderUid: string +} + +export type PlacePamConfigurationInFolderOptions = { + srcFolderUid?: string + previous?: PamConfigFolderTarget +} + +export type ResolvePamConfigFolderOptions = { + required?: boolean +} + +export type CreatePamConfigurationInput = { + title: string + configType: string + sharedFolder: string + gateway?: string + fields?: PamConfigurationRecordFieldInput[] + custom?: PamConfigurationRecordFieldInput[] + notes?: string + adminCredentialUid?: string + permissions?: PamConfigurationPermissionsInput + returnValue?: boolean +} + +export type CreatePamConfigurationResult = { + success: boolean + configurationUid: string + title: string + configType: string + sharedFolderUid: string + gatewayUid: string + gatewayLinked: boolean + permissionsApplied: boolean + warnings: string[] + message: string +} + +export type EditPamConfigurationInput = { + configurationUidOrTitle: string + title?: string + configType?: string + sharedFolder?: string + gateway?: string + fields?: PamConfigurationRecordFieldInput[] + custom?: PamConfigurationRecordFieldInput[] + notes?: string + adminCredentialUid?: string + removeResourceRecords?: string[] + permissions?: PamConfigurationPermissionsInput +} + +export type EditPamConfigurationResult = { + success: boolean + configurationUid: string + title: string + configType: string + previousConfigType: string + sharedFolderUid: string + previousSharedFolderUid: string + gatewayUid: string + previousGatewayUid: string + gatewayChanged: boolean + folderChanged: boolean + titleChanged: boolean + typeChanged: boolean + removedResourceRecordUids: string[] + permissionsApplied: boolean + warnings: string[] + message: string +} + +export type RemovePamConfigurationInput = { + configurationUidOrTitle: string +} + +export type RemovePamConfigurationResult = { + success: boolean + found: boolean + configurationUid?: string + title?: string + configType?: string + message: string +} diff --git a/KeeperSdk/src/pam/config/createConfig.ts b/KeeperSdk/src/pam/config/createConfig.ts new file mode 100644 index 0000000..986cf10 --- /dev/null +++ b/KeeperSdk/src/pam/config/createConfig.ts @@ -0,0 +1,219 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { + addConfigurationRecordMessage, + generateEncryptionKey, + generateUidBytes, + platform, + syncDown, + webSafe64FromBytes, +} from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { applyPamConfigurationPermissions, hasPermissionsInput } from './applyConfigPermissions' +import { isPamConfigurationRecordType, resolvePamConfigurationRecordType } from './configHelpers' +import { + ensureScheduleField, + getPaddedJsonBytes, + linkConfigurationController, + normalizeFields, + resolveGatewayUidSoft, + upsertPamResourcesField, +} from './configMutationHelpers' +import { + createPamConfigurationInNsfFolder, + isPamConfigurationInFolder, + placePamConfigurationInFolder, + resolvePamConfigFolder, +} from './pamConfigFolder' +import type { CreatePamConfigurationInput, CreatePamConfigurationResult } from './configTypes' + +export async function createPamConfiguration( + auth: Auth, + storage: InMemoryStorage, + input: CreatePamConfigurationInput & { returnValue: true } +): Promise +export async function createPamConfiguration( + auth: Auth, + storage: InMemoryStorage, + input: CreatePamConfigurationInput & { returnValue?: false } +): Promise +export async function createPamConfiguration( + auth: Auth, + storage: InMemoryStorage, + input: CreatePamConfigurationInput +): Promise +export async function createPamConfiguration( + auth: Auth, + storage: InMemoryStorage, + input: CreatePamConfigurationInput +): Promise { + const title = input.title?.trim() || '' + if (!title) { + throw new KeeperSdkError('PAM Configuration title is required.', ResultCodes.PAM_CONFIG_TITLE_REQUIRED) + } + + const configTypeRaw = input.configType?.trim() || '' + if (!configTypeRaw) { + throw new KeeperSdkError( + 'PAM Configuration type is required (e.g. pamAwsConfiguration or environment aws).', + ResultCodes.PAM_CONFIG_TYPE_REQUIRED + ) + } + + const configType = resolvePamConfigurationRecordType(configTypeRaw) + if (!configType || !isPamConfigurationRecordType(configType)) { + throw new KeeperSdkError( + `Invalid PAM Configuration type "${configTypeRaw}". Use a known type or environment (aws, azure, gcp, domain, local, oci, github).`, + ResultCodes.PAM_CONFIG_TYPE_INVALID + ) + } + + const folderTarget = resolvePamConfigFolder(storage, input.sharedFolder) + const sharedFolderUid = folderTarget.uid + const warnings: string[] = [] + const gatewayUid = await resolveGatewayUidSoft(auth, input.gateway, warnings, { + failureResultCode: ResultCodes.PAM_CONFIG_CREATE_FAILED, + missingWarning: 'No gateway provided. Configuration will be created without a gateway controller link.', + notFoundWarning: (gateway) => + `Gateway "${gateway}" not found. Configuration will be created without a gateway controller link.`, + resolveFailedWarning: (gateway, error) => + `Failed to resolve gateway "${gateway}": ${error}. Configuration will be created without a gateway controller link.`, + }) + + const fields = upsertPamResourcesField(ensureScheduleField(normalizeFields(input.fields)), { + gatewayUid, + sharedFolderUid, + resourceRecordUids: [], + adminCredentialUid: input.adminCredentialUid?.trim() || undefined, + }) + const custom = normalizeFields(input.custom) + + const configurationUidBytes = generateUidBytes() + const recordKey = generateEncryptionKey() + const configurationUid = webSafe64FromBytes(configurationUidBytes) + + const recordPayload = { + type: configType, + title, + fields, + custom, + notes: input.notes || '', + } + + try { + if (folderTarget.kind === 'nsf') { + await createPamConfigurationInNsfFolder(auth, storage, { + configurationUid, + configurationUidBytes, + recordKey, + recordPayload, + folderUid: sharedFolderUid, + }) + } else { + await auth.executeRestAction( + addConfigurationRecordMessage({ + configurationUid: configurationUidBytes, + recordKey: await platform.aesGcmEncrypt(recordKey, auth.dataKey!), + data: await platform.aesGcmEncrypt(getPaddedJsonBytes(recordPayload), recordKey), + }) + ) + } + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to create PAM Configuration: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_CREATE_FAILED + ) + } + + await storage.saveKeyBytes(configurationUid, recordKey) + + try { + await syncDown({ auth, storage }) + } catch (err) { + warnings.push(`Created configuration ${configurationUid} but vault sync failed: ${extractErrorMessage(err)}`) + } + + if (folderTarget.kind === 'shared_folder') { + const moveResult = await placePamConfigurationInFolder(auth, storage, configurationUid, folderTarget, { + srcFolderUid: '', + }) + if (!moveResult.success) { + throw new KeeperSdkError( + `Created configuration ${configurationUid} but failed to move into shared folder: ${moveResult.message || 'unknown error'}`, + ResultCodes.PAM_CONFIG_MOVE_FAILED + ) + } + + try { + await syncDown({ auth, storage }) + } catch (err) { + warnings.push( + `Moved configuration ${configurationUid} but post-move sync failed: ${extractErrorMessage(err)}` + ) + } + } + + if (!isPamConfigurationInFolder(storage, configurationUid, folderTarget)) { + throw new KeeperSdkError( + `Created configuration ${configurationUid} but it is still not linked to folder ${sharedFolderUid}.`, + ResultCodes.PAM_CONFIG_MOVE_FAILED + ) + } + + let gatewayLinked = false + if (gatewayUid) { + try { + await linkConfigurationController(auth, configurationUid, gatewayUid) + gatewayLinked = true + } catch (err) { + warnings.push( + `Created configuration ${configurationUid} but failed to link gateway ${gatewayUid}: ${extractErrorMessage(err)}` + ) + } + } + + let permissionsApplied = false + if (hasPermissionsInput(input.permissions)) { + permissionsApplied = await applyPamConfigurationPermissions( + auth, + configurationUid, + input.permissions!, + warnings, + { warnOnDefaultReset: false } + ) + } + + if (input.returnValue === true) { + return configurationUid + } + + return { + success: true, + configurationUid, + title, + configType, + sharedFolderUid, + gatewayUid, + gatewayLinked, + permissionsApplied, + warnings, + message: `PAM Configuration "${title}" created (${configurationUid}).`, + } +} + +export function formatCreatePamConfigurationOutput(result: CreatePamConfigurationResult): string { + const lines = [ + result.message, + `UID: ${result.configurationUid}`, + `Type: ${result.configType}`, + `Shared Folder: ${result.sharedFolderUid}`, + `Gateway UID: ${result.gatewayUid || '(none)'}`, + `Gateway Linked: ${result.gatewayLinked ? 'yes' : 'no'}`, + `Permissions Applied: ${result.permissionsApplied ? 'yes' : 'no'}`, + ] + for (const warning of result.warnings) { + lines.push(`Warning: ${warning}`) + } + return lines.join('\n') +} diff --git a/KeeperSdk/src/pam/config/editConfig.ts b/KeeperSdk/src/pam/config/editConfig.ts new file mode 100644 index 0000000..b365f86 --- /dev/null +++ b/KeeperSdk/src/pam/config/editConfig.ts @@ -0,0 +1,266 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { applyPamConfigurationPermissions, hasPermissionsInput } from './applyConfigPermissions' +import { isPamConfigurationRecordType, resolvePamConfigurationRecordType } from './configHelpers' +import { + findPamConfigurationByUidOrTitle, + linkConfigurationController, + mergeRecordFields, + parsePamResourcesFromRecord, + readTypedRecordPayload, + resolveGatewayUidSoft, + resolveResourceRecordUidsToRemove, + upsertPamResourcesField, +} from './configMutationHelpers' +import { + findPamConfigFolderForRecord, + placePamConfigurationInFolder, + resolvePamConfigFolder, + updatePamConfigurationRecordData, + type PamConfigFolderTarget, +} from './pamConfigFolder' +import { isNestedShareRecord } from '../../nestedShareFolders/nsfHelpers' +import type { EditPamConfigurationInput, EditPamConfigurationResult } from './configTypes' + +function hasRecordEditWork(input: EditPamConfigurationInput): boolean { + return ( + input.title != null || + input.configType != null || + input.sharedFolder != null || + input.gateway != null || + (input.fields != null && input.fields.length > 0) || + (input.custom != null && input.custom.length > 0) || + input.notes != null || + input.adminCredentialUid != null || + (input.removeResourceRecords != null && input.removeResourceRecords.length > 0) + ) +} + +export async function editPamConfiguration( + auth: Auth, + storage: InMemoryStorage, + input: EditPamConfigurationInput +): Promise { + const configurationUidOrTitle = input.configurationUidOrTitle?.trim() || '' + if (!configurationUidOrTitle) { + throw new KeeperSdkError('PAM Configuration UID or title is required.', ResultCodes.PAM_CONFIG_REQUIRED) + } + + const recordWork = hasRecordEditWork(input) + const permissionsWork = hasPermissionsInput(input.permissions) + if (!recordWork && !permissionsWork) { + throw new KeeperSdkError( + 'Nothing to do. Provide at least one of title, configType, sharedFolder, gateway, fields, custom, notes, removeResourceRecords, or permissions.', + ResultCodes.PAM_CONFIG_EDIT_NOTHING_TO_DO + ) + } + + const record = findPamConfigurationByUidOrTitle(storage, configurationUidOrTitle) + const configurationUid = record.uid + const existing = readTypedRecordPayload(record) + const existingResources = parsePamResourcesFromRecord(record) + + const warnings: string[] = [] + + let title = existing.title + let titleChanged = false + let configType = existing.configType + let typeChanged = false + const previousFolder: PamConfigFolderTarget | undefined = + findPamConfigFolderForRecord(storage, configurationUid) || + (existingResources.sharedFolderUid + ? { + kind: isNestedShareRecord(storage, configurationUid) ? 'nsf' : 'shared_folder', + uid: existingResources.sharedFolderUid, + } + : undefined) + const previousSharedFolderUid = previousFolder?.uid || existingResources.sharedFolderUid + let sharedFolderUid = previousSharedFolderUid + let folderTarget: PamConfigFolderTarget | undefined = previousFolder + let folderChanged = false + const previousGatewayUid = existingResources.gatewayUid + let gatewayUid = previousGatewayUid + let gatewayChanged = false + let removedResourceRecordUids: string[] = [] + + if (recordWork) { + if (input.title != null) { + const nextTitle = String(input.title).trim() + if (!nextTitle) { + throw new KeeperSdkError( + 'PAM Configuration title cannot be empty.', + ResultCodes.PAM_CONFIG_TITLE_REQUIRED + ) + } + titleChanged = nextTitle !== existing.title + title = nextTitle + } + + if (input.configType != null && String(input.configType).trim()) { + const resolved = resolvePamConfigurationRecordType(String(input.configType).trim()) + if (!resolved || !isPamConfigurationRecordType(resolved)) { + throw new KeeperSdkError( + `Invalid PAM Configuration type "${input.configType}". Use a known type or environment (aws, azure, gcp, domain, local, oci, github).`, + ResultCodes.PAM_CONFIG_TYPE_INVALID + ) + } + typeChanged = resolved !== existing.configType + configType = resolved + } + + if (input.sharedFolder != null && String(input.sharedFolder).trim()) { + folderTarget = resolvePamConfigFolder(storage, String(input.sharedFolder), { required: true }) + sharedFolderUid = folderTarget.uid + folderChanged = sharedFolderUid !== previousSharedFolderUid + } + + if (input.gateway != null) { + const resolvedGatewayUid = await resolveGatewayUidSoft(auth, input.gateway, warnings, { + failureResultCode: ResultCodes.PAM_CONFIG_EDIT_FAILED, + notFoundWarning: (gateway) => + `Gateway "${gateway}" not found. Gateway controller link was left unchanged.`, + resolveFailedWarning: (gateway, error) => + `Failed to resolve gateway "${gateway}": ${error}. Gateway controller link was left unchanged.`, + }) + if (resolvedGatewayUid) { + gatewayChanged = resolvedGatewayUid !== previousGatewayUid + gatewayUid = resolvedGatewayUid + } else if (String(input.gateway).trim() === '') { + gatewayChanged = previousGatewayUid !== '' + gatewayUid = '' + } + } + + removedResourceRecordUids = resolveResourceRecordUidsToRemove( + storage, + input.removeResourceRecords, + existingResources.resourceRecordUids, + warnings + ) + const removedSet = new Set(removedResourceRecordUids) + const resourceRecordUids = existingResources.resourceRecordUids.filter((uid) => !removedSet.has(uid)) + + let adminCredentialUid = existingResources.adminCredentialUid + if (input.adminCredentialUid != null) { + const trimmed = String(input.adminCredentialUid).trim() + adminCredentialUid = trimmed || undefined + } + + let fields = mergeRecordFields(existing.fields, input.fields) + fields = upsertPamResourcesField(fields, { + gatewayUid, + sharedFolderUid, + resourceRecordUids, + adminCredentialUid, + }) + const custom = mergeRecordFields(existing.custom, input.custom) + const notes = input.notes != null ? String(input.notes) : existing.notes + + const recordKey = await storage.getKeyBytes(configurationUid) + if (!recordKey) { + throw new KeeperSdkError( + `Record key not found for PAM Configuration "${configurationUid}". Sync the vault and try again.`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } + + try { + await updatePamConfigurationRecordData( + auth, + storage, + configurationUid, + { + type: configType, + title, + fields, + custom, + notes, + }, + record.revision, + recordKey + ) + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to update PAM Configuration: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } + + if (gatewayChanged && gatewayUid) { + try { + await linkConfigurationController(auth, configurationUid, gatewayUid) + } catch (err) { + warnings.push( + `Updated configuration ${configurationUid} but failed to link gateway ${gatewayUid}: ${extractErrorMessage(err)}` + ) + } + } + + if (folderChanged && folderTarget) { + const moveResult = await placePamConfigurationInFolder(auth, storage, configurationUid, folderTarget, { + previous: previousFolder, + }) + if (!moveResult.success) { + throw new KeeperSdkError( + `Updated configuration ${configurationUid} but failed to move into folder: ${moveResult.message || 'unknown error'}`, + ResultCodes.PAM_CONFIG_MOVE_FAILED + ) + } + } + } + + let permissionsApplied = false + if (permissionsWork) { + permissionsApplied = await applyPamConfigurationPermissions( + auth, + configurationUid, + input.permissions!, + warnings + ) + } + + return { + success: true, + configurationUid, + title, + configType, + previousConfigType: existing.configType, + sharedFolderUid, + previousSharedFolderUid, + gatewayUid, + previousGatewayUid, + gatewayChanged, + folderChanged, + titleChanged, + typeChanged, + removedResourceRecordUids, + permissionsApplied, + warnings, + message: `PAM Configuration "${title}" updated (${configurationUid}).`, + } +} + +export function formatEditPamConfigurationOutput(result: EditPamConfigurationResult): string { + const lines = [ + result.message, + `UID: ${result.configurationUid}`, + result.typeChanged ? `Type: ${result.previousConfigType} → ${result.configType}` : `Type: ${result.configType}`, + result.titleChanged ? `Title changed: yes` : `Title: ${result.title}`, + result.folderChanged + ? `Shared Folder: ${result.previousSharedFolderUid || '(none)'} → ${result.sharedFolderUid}` + : `Shared Folder: ${result.sharedFolderUid || '(none)'}`, + result.gatewayChanged + ? `Gateway UID: ${result.previousGatewayUid || '(none)'} → ${result.gatewayUid || '(none)'}` + : `Gateway UID: ${result.gatewayUid || '(none)'}`, + `Permissions Applied: ${result.permissionsApplied ? 'yes' : 'no'}`, + ] + if (result.removedResourceRecordUids.length) { + lines.push(`Removed Resource UIDs: ${result.removedResourceRecordUids.join(', ')}`) + } + for (const warning of result.warnings) { + lines.push(`Warning: ${warning}`) + } + return lines.join('\n') +} diff --git a/KeeperSdk/src/pam/config/index.ts b/KeeperSdk/src/pam/config/index.ts new file mode 100644 index 0000000..efca4c4 --- /dev/null +++ b/KeeperSdk/src/pam/config/index.ts @@ -0,0 +1,120 @@ +export { ConfigManager } from './ConfigManager' + +export { + listPamConfigurations, + formatPamConfigurationsTable, + renderPamConfigurationsAsciiTable, + formatPamConfigurationsJson, + formatPamConfigurationsOutput, +} from './listConfigs' + +export { createPamConfiguration, formatCreatePamConfigurationOutput } from './createConfig' +export { editPamConfiguration, formatEditPamConfigurationOutput } from './editConfig' +export { removePamConfiguration, formatRemovePamConfigurationOutput } from './removeConfig' + +export { PamConfigListFormat } from './configTypes' +export type { + PamConfigListFormatInput, + ListPamConfigurationsOptions, + PamResourcesInfo, + PamConfigurationField, + PamConfigurationListRow, + PamConfigurationDetail, + ListPamConfigurationsResult, + FormattedPamConfigurationsTable, + FormatPamConfigurationsTableOptions, + RenderPamConfigurationsAsciiTableOptions, + PamConfigurationJsonField, + PamConfigurationJsonEntry, + PamConfigurationsJsonPayload, + PamConfigurationRecordFieldInput, + PamConfigurationPermissionValue, + PamConfigurationPermissionsInput, + PamNetworkAllowedSettings, + PamNetworkAllowedSettingsKey, + PamPermissionBuildResult, + ApplyPamConfigurationPermissionsOptions, + PamConfigFolderKind, + PamConfigFolderTarget, + PamConfigFolderPlacementResult, + PamConfigRecordRemovalResult, + PamConfigurationTypedRecordData, + CreatePamConfigurationInNsfFolderOptions, + PlacePamConfigurationInFolderOptions, + ResolvePamConfigFolderOptions, + CreatePamConfigurationInput, + CreatePamConfigurationResult, + EditPamConfigurationInput, + EditPamConfigurationResult, + RemovePamConfigurationInput, + RemovePamConfigurationResult, +} from './configTypes' + +export { + PAM_CONFIGURATION_RECORD_VERSION, + PAM_CONFIGURATION_RECORD_TYPES, + PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE, + PAM_CONFIG_ENVIRONMENTS, + PAM_RESOURCES_FIELD_TYPE, + FILE_REF_FIELD_TYPE, + SCHEDULE_FIELD_TYPE, + DEFAULT_PAM_CONFIG_SCHEDULE_VALUE, + EMPTY_PAM_CONFIGURATIONS_MESSAGE, + PAM_CONFIG_LIST_DEFAULT_HEADERS, + PAM_CONFIG_LIST_VERBOSE_HEADERS, + PAM_CONFIG_DETAIL_HEADERS, + PAM_CONFIG_DETAIL_LABELS, + PAM_CONFIG_PERMISSION_DAG_KEYS, + PAM_CONFIG_PERMISSION_FLAGS, + PAM_CONFIG_PERMISSION_VALUES, +} from './configConstants' +export type { PamConfigurationRecordType, PamConfigEnvironment, PamConfigPermissionFlag } from './configConstants' + +export { + isPamConfigurationRecordType, + isPamConfigEnvironment, + resolvePamConfigurationRecordType, + isPamConfigurationRecord, + getPamConfigurationFields, + parsePamResources, + resolveSharedFolderName, + findSharedFolderUidForRecord, + listPamConfigurationRecords, + getPamConfigurationDisplayName, +} from './configHelpers' + +export { + getPaddedJsonBytes, + normalizeFields, + ensureScheduleField, + mergeRecordFields, + readTypedRecordPayload, + upsertPamResourcesField, + resolveSharedFolderUid, + resolveGatewayUidSoft, + findPamConfigurationByUidOrTitle, + resolveResourceRecordUidsToRemove, + linkConfigurationController, + moveConfigurationToSharedFolder, +} from './configMutationHelpers' + +export { + resolvePamConfigFolder, + findPamConfigFolderForRecord, + resolvePamConfigFolderTargetFromUid, + resolvePamConfigFolderName, + formatPamConfigFolderDisplay, + createPamConfigurationInNsfFolder, + updatePamConfigurationRecordData, + placePamConfigurationInFolder, + removePamConfigurationRecord, + isPamConfigurationInFolder, +} from './pamConfigFolder' + +export { + hasPermissionsInput, + convertPermissionValue, + normalizePermissionValue, + buildAllowedSettingsFromPermissions, + applyPamConfigurationPermissions, +} from './applyConfigPermissions' diff --git a/KeeperSdk/src/pam/config/listConfigs.ts b/KeeperSdk/src/pam/config/listConfigs.ts new file mode 100644 index 0000000..272f575 --- /dev/null +++ b/KeeperSdk/src/pam/config/listConfigs.ts @@ -0,0 +1,350 @@ +import type { DRecord } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { getRecordType } from '../../records/RecordUtils' +import { KeeperSdkError, ResultCodes } from '../../utils' +import { + EMPTY_PAM_CONFIGURATIONS_MESSAGE, + PAM_CONFIG_DETAIL_LABELS, + PAM_CONFIG_LIST_DEFAULT_HEADERS, + PAM_CONFIG_LIST_VERBOSE_HEADERS, + PAM_CONFIGURATION_RECORD_VERSION, +} from './configConstants' +import { + getPamConfigurationDisplayName, + getPamConfigurationFields, + isPamConfigurationRecord, + isPamConfigurationRecordType, + listPamConfigurationRecords, + parsePamResources, +} from './configHelpers' +import { + findPamConfigFolderForRecord, + formatPamConfigFolderDisplay, + resolvePamConfigFolderName, + resolvePamConfigFolderTargetFromUid, + type PamConfigFolderTarget, +} from './pamConfigFolder' +import { + PamConfigListFormat, + type FormatPamConfigurationsTableOptions, + type FormattedPamConfigurationsTable, + type ListPamConfigurationsOptions, + type ListPamConfigurationsResult, + type PamConfigurationDetail, + type PamConfigurationField, + type PamConfigurationJsonEntry, + type PamConfigurationListRow, + type PamConfigurationsJsonPayload, + type RenderPamConfigurationsAsciiTableOptions, +} from './configTypes' + +function resolveFolderForConfiguration( + storage: InMemoryStorage, + record: DRecord +): { folder?: PamConfigFolderTarget; inSharedFolder: boolean } { + const membership = findPamConfigFolderForRecord(storage, record.uid) + if (membership) return { folder: membership, inSharedFolder: true } + + const resources = parsePamResources(record) + const fallback = resolvePamConfigFolderTargetFromUid(storage, resources.sharedFolderUid) + return { folder: fallback, inSharedFolder: false } +} + +function buildListRow( + storage: InMemoryStorage, + record: DRecord, + verbose: boolean +): { row?: PamConfigurationListRow; warning?: string } { + const configType = getRecordType(record) + if (!isPamConfigurationRecordType(configType)) { + return { warning: `Unsupported PAM configuration type "${configType}" for record ${record.uid}` } + } + + const resources = parsePamResources(record) + const { folder, inSharedFolder } = resolveFolderForConfiguration(storage, record) + const warning = !inSharedFolder + ? `Following configuration is not in the shared folder: UID: ${record.uid}, Title: ${getPamConfigurationDisplayName(record)}` + : undefined + + if (!folder) { + return { + warning: + warning || + `Following configuration is not in the shared folder: UID: ${record.uid}, Title: ${getPamConfigurationDisplayName(record)}`, + } + } + + const row: PamConfigurationListRow = { + uid: record.uid, + name: getPamConfigurationDisplayName(record), + configType, + sharedFolderUid: folder.uid, + sharedFolderName: resolvePamConfigFolderName(storage, folder), + gatewayUid: resources.gatewayUid, + resourceRecordUids: resources.resourceRecordUids, + } + + if (verbose) { + row.fields = getPamConfigurationFields(record) + } + + return { row, warning } +} + +function buildDetail(storage: InMemoryStorage, record: DRecord): PamConfigurationDetail { + const resources = parsePamResources(record) + const { folder } = resolveFolderForConfiguration(storage, record) + const sharedFolderUid = folder?.uid || '' + return { + uid: record.uid, + name: getPamConfigurationDisplayName(record), + configType: getRecordType(record), + sharedFolderUid, + sharedFolderName: folder ? resolvePamConfigFolderName(storage, folder) : '', + gatewayUid: resources.gatewayUid, + resourceRecordUids: resources.resourceRecordUids, + fields: getPamConfigurationFields(record), + } +} + +function loadConfigurationDetail(storage: InMemoryStorage, configUid: string): PamConfigurationDetail { + const record = storage.getByUid(VaultObjectKind.Record, configUid) + if (!record) { + throw new KeeperSdkError(`PAM Configuration "${configUid}" not found.`, ResultCodes.PAM_CONFIG_NOT_FOUND) + } + if (record.version !== PAM_CONFIGURATION_RECORD_VERSION || !isPamConfigurationRecord(record)) { + throw new KeeperSdkError( + `Record "${configUid}" is not a PAM Configuration (expected version ${PAM_CONFIGURATION_RECORD_VERSION}).`, + ResultCodes.PAM_CONFIG_INVALID + ) + } + return buildDetail(storage, record) +} + +export function listPamConfigurations( + storage: InMemoryStorage, + options: ListPamConfigurationsOptions = {} +): ListPamConfigurationsResult { + const verbose = options.verbose === true + const configUid = options.configUid?.trim() || '' + + if (configUid) { + return { + configurations: [], + detail: loadConfigurationDetail(storage, configUid), + warnings: [], + } + } + + const warnings: string[] = [] + const configurations: PamConfigurationListRow[] = [] + + for (const record of listPamConfigurationRecords(storage)) { + const { row, warning } = buildListRow(storage, record, verbose) + if (warning) warnings.push(warning) + if (row) configurations.push(row) + } + + configurations.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + + return { + configurations, + warnings, + message: configurations.length === 0 ? EMPTY_PAM_CONFIGURATIONS_MESSAGE : undefined, + } +} + +function formatVerboseFieldLine(field: PamConfigurationField): string { + const value = field.values.join(', ') + if (!value) return '' + if (field.label) return `(${field.type}).${field.label}: ${value}` + return `(${field.type}): ${value}` +} + +function formatFieldSummary(fields: PamConfigurationListRow['fields']): string { + if (!fields?.length) return '' + return fields.map(formatVerboseFieldLine).filter(Boolean).join('\n') +} + +function formatFolderCell(name: string, uid: string): string { + return formatPamConfigFolderDisplay(name, uid) +} + +function formatPamConfigurationDetail(detail: PamConfigurationDetail, options: { verbose?: boolean } = {}): string { + const rows: Array<[string, string]> = [ + [PAM_CONFIG_DETAIL_LABELS[0], detail.uid], + [PAM_CONFIG_DETAIL_LABELS[1], detail.name], + [PAM_CONFIG_DETAIL_LABELS[2], detail.configType], + [PAM_CONFIG_DETAIL_LABELS[3], formatFolderCell(detail.sharedFolderName, detail.sharedFolderUid)], + [PAM_CONFIG_DETAIL_LABELS[4], detail.gatewayUid], + [PAM_CONFIG_DETAIL_LABELS[5], detail.resourceRecordUids.join(', ')], + ] + + if (options.verbose) { + for (const field of detail.fields) { + const line = formatVerboseFieldLine(field) + if (!line) continue + const separator = line.indexOf(': ') + if (separator < 0) continue + rows.push([line.slice(0, separator), line.slice(separator + 2)]) + } + } + + const labelWidth = Math.max(...rows.map(([label]) => label.length), 1) + return rows.map(([label, value]) => `${label.padStart(labelWidth)} ${value}`).join('\n') +} + +export function formatPamConfigurationsTable( + result: ListPamConfigurationsResult, + options: FormatPamConfigurationsTableOptions = {} +): FormattedPamConfigurationsTable { + const verbose = options.verbose === true + + if (result.detail) { + const detail = result.detail + const rows: string[][] = [ + ['UID', detail.uid], + ['Name', detail.name], + ['Config Type', detail.configType], + ['Folder', formatFolderCell(detail.sharedFolderName, detail.sharedFolderUid)], + ['Gateway UID', detail.gatewayUid], + ['Resource Record UIDs', detail.resourceRecordUids.join(', ')], + ] + if (verbose) { + for (const field of detail.fields) { + const line = formatVerboseFieldLine(field) + if (!line) continue + const separator = line.indexOf(': ') + if (separator < 0) continue + rows.push([line.slice(0, separator), line.slice(separator + 2)]) + } + } + return { headers: ['Field', 'Value'], rows } + } + + const headers: string[] = [...PAM_CONFIG_LIST_DEFAULT_HEADERS] + if (verbose) headers.push(...PAM_CONFIG_LIST_VERBOSE_HEADERS) + + const rows = result.configurations.map((config) => { + const row: string[] = [ + config.uid, + config.name, + config.configType, + formatFolderCell(config.sharedFolderName, config.sharedFolderUid), + config.gatewayUid, + config.resourceRecordUids.join(', '), + ] + if (verbose) row.push(formatFieldSummary(config.fields)) + return row + }) + + return { headers, rows } +} + +export function renderPamConfigurationsAsciiTable( + table: FormattedPamConfigurationsTable, + options: RenderPamConfigurationsAsciiTableOptions = {} +): string { + const minColWidth = options.minColWidth ?? 2 + const splitRows = table.rows.map((row) => row.map((cell) => (cell || '').split('\n'))) + const widths = table.headers.map((header, col) => { + let width = Math.max(header.length, minColWidth) + for (const row of splitRows) { + for (const line of row[col] || ['']) { + width = Math.max(width, line.length) + } + } + return width + }) + + const formatCells = (cells: string[]): string => + cells + .map((cell, i) => (cell || '').padEnd(widths[i])) + .join(' ') + .trimEnd() + + const lines: string[] = [formatCells([...table.headers]), widths.map((w) => '-'.repeat(w)).join(' ')] + + for (const row of splitRows) { + const lineCount = Math.max(1, ...row.map((cell) => cell.length)) + for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) { + lines.push(formatCells(row.map((cell) => cell[lineIndex] || ''))) + } + } + + return lines.join('\n') +} + +function toJsonEntry(config: PamConfigurationListRow | PamConfigurationDetail): PamConfigurationJsonEntry { + const entry: PamConfigurationJsonEntry = { + uid: config.uid, + name: config.name, + config_type: config.configType, + shared_folder_uid: config.sharedFolderUid, + shared_folder_name: config.sharedFolderName, + gateway_uid: config.gatewayUid, + resource_record_uids: config.resourceRecordUids, + } + if ('fields' in config && config.fields) { + entry.fields = config.fields.map((field) => ({ + type: field.type, + label: field.label, + values: field.values, + })) + } + return entry +} + +export function formatPamConfigurationsJson( + result: ListPamConfigurationsResult, + options: ListPamConfigurationsOptions = {} +): string { + const verbose = options.verbose === true + const payload: PamConfigurationsJsonPayload = {} + + if (result.detail) { + payload.configuration = { + ...toJsonEntry(result.detail), + fields: result.detail.fields.map((field) => ({ + type: field.type, + label: field.label, + values: field.values, + })), + } + } else { + payload.configurations = result.configurations.map((config) => { + const entry = toJsonEntry(config) + if (!verbose) delete entry.fields + return entry + }) + } + + if (result.warnings.length) payload.warnings = result.warnings + if (result.message) payload.message = result.message + + return JSON.stringify(payload, null, 2) +} + +export function formatPamConfigurationsOutput( + result: ListPamConfigurationsResult, + options: ListPamConfigurationsOptions = {} +): string { + const format = String(options.format || PamConfigListFormat.Table).toLowerCase() + if (format === PamConfigListFormat.Json) return formatPamConfigurationsJson(result, options) + + const parts: string[] = [] + for (const warning of result.warnings) { + parts.push(`Warning: ${warning}`) + } + if (result.message && result.configurations.length === 0 && !result.detail) { + parts.push(result.message) + return parts.join('\n') + } + if (result.detail) { + parts.push(formatPamConfigurationDetail(result.detail, { verbose: options.verbose })) + return parts.join('\n') + } + parts.push(renderPamConfigurationsAsciiTable(formatPamConfigurationsTable(result, { verbose: options.verbose }))) + return parts.join('\n') +} diff --git a/KeeperSdk/src/pam/config/pamConfigFolder.ts b/KeeperSdk/src/pam/config/pamConfigFolder.ts new file mode 100644 index 0000000..5caa5e0 --- /dev/null +++ b/KeeperSdk/src/pam/config/pamConfigFolder.ts @@ -0,0 +1,310 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { + Folder, + Records, + addPamConfigurationV3Message, + keeperDriveRecordsUpdate, + normal64Bytes, + platform, + recordsUpdateMessage, +} from '@keeper-security/keeperapi' +import { findFolder } from '../../folders/getFolder' +import { FolderKind } from '../../folders/folderHelpers' +import { deleteRecord, moveRecord } from '../../records/RecordOperations' +import { + findNestedShareFoldersForRecord, + getFolderDisplayName, + getKeeperDriveFolder, + isNestedShareRecord, + isRootFolderUid, + resolveNsfFolderIdentifier, +} from '../../nestedShareFolders/nsfHelpers' +import { linkNestedShareRecord } from '../../nestedShareFolders/linkNsfRecord' +import { removeNestedShareRecords } from '../../nestedShareFolders/removeNsfRecord' +import { NsfRemoveOperation } from '../../nestedShareFolders/nsfTypes' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { findSharedFolderUidForRecord, resolveSharedFolderName } from './configHelpers' +import { getPaddedJsonBytes } from './configRecordPayload' +import type { + CreatePamConfigurationInNsfFolderOptions, + PamConfigFolderPlacementResult, + PamConfigFolderTarget, + PamConfigRecordRemovalResult, + PamConfigurationTypedRecordData, + PlacePamConfigurationInFolderOptions, + ResolvePamConfigFolderOptions, +} from './configTypes' + +export type { PamConfigFolderKind, PamConfigFolderTarget } from './configTypes' + +function isRecordModifySuccess(status: Records.RecordModifyResult | null | undefined): boolean { + return status == null || status === Records.RecordModifyResult.RS_SUCCESS +} + +export function resolvePamConfigFolder( + storage: InMemoryStorage, + sharedFolder: string, + options: ResolvePamConfigFolderOptions = {} +): PamConfigFolderTarget { + const trimmed = sharedFolder.trim() + const required = options.required !== false + if (!trimmed) { + if (required) { + throw new KeeperSdkError( + 'Shared folder or Nested Share Folder UID or name is required.', + ResultCodes.PAM_CONFIG_SHARED_FOLDER_REQUIRED + ) + } + return { kind: 'shared_folder', uid: '' } + } + + const classic = findFolder(storage, trimmed) + if (classic?.kind === FolderKind.SharedFolder) { + return { kind: 'shared_folder', uid: classic.folder.uid } + } + + const nsfUid = resolveNsfFolderIdentifier(storage, trimmed) + if (nsfUid != null && nsfUid !== '') { + if (isRootFolderUid(storage, nsfUid)) { + throw new KeeperSdkError( + `Nested Share Folder root cannot host a PAM Configuration. Choose a nested folder.`, + ResultCodes.PAM_CONFIG_SHARED_FOLDER_NOT_FOUND + ) + } + return { kind: 'nsf', uid: nsfUid } + } + + throw new KeeperSdkError( + `Shared folder or Nested Share Folder "${trimmed}" not found.`, + ResultCodes.PAM_CONFIG_SHARED_FOLDER_NOT_FOUND + ) +} + +export function findPamConfigFolderForRecord( + storage: InMemoryStorage, + recordUid: string +): PamConfigFolderTarget | undefined { + const sharedFolderUid = findSharedFolderUidForRecord(storage, recordUid) + if (sharedFolderUid) return { kind: 'shared_folder', uid: sharedFolderUid } + + const nsfFolders = findNestedShareFoldersForRecord(storage, recordUid).filter( + (uid) => uid && !isRootFolderUid(storage, uid) + ) + if (nsfFolders.length > 0) return { kind: 'nsf', uid: nsfFolders[0] } + return undefined +} + +export function resolvePamConfigFolderTargetFromUid( + storage: InMemoryStorage, + folderUid: string +): PamConfigFolderTarget | undefined { + const trimmed = folderUid.trim() + if (!trimmed) return undefined + + const classic = findFolder(storage, trimmed) + if (classic?.kind === FolderKind.SharedFolder) { + return { kind: 'shared_folder', uid: classic.folder.uid } + } + + if (getKeeperDriveFolder(storage, trimmed) && !isRootFolderUid(storage, trimmed)) { + return { kind: 'nsf', uid: trimmed } + } + + return { kind: 'shared_folder', uid: trimmed } +} + +export function resolvePamConfigFolderName(storage: InMemoryStorage, folder: PamConfigFolderTarget): string { + if (!folder.uid) return '' + if (folder.kind === 'shared_folder') return resolveSharedFolderName(storage, folder.uid) + const name = getFolderDisplayName(storage, folder.uid) || folder.uid + return name.endsWith(' [NSF]') ? name : `${name} [NSF]` +} + +export function formatPamConfigFolderDisplay(name: string, uid: string): string { + if (!name && !uid) return '' + const isNsf = name.endsWith(' [NSF]') + const displayName = isNsf ? name.slice(0, -6) : name || uid + if (!uid) return name || displayName + return `${displayName} (${uid})${isNsf ? ' [NSF]' : ''}` +} + +export async function createPamConfigurationInNsfFolder( + auth: Auth, + storage: InMemoryStorage, + options: CreatePamConfigurationInNsfFolderOptions +): Promise { + const folderKey = await storage.getKeyBytes(options.folderUid) + if (!folderKey) { + throw new KeeperSdkError( + `Folder key not found for Nested Share Folder ${options.folderUid}. Sync the vault and try again.`, + ResultCodes.PAM_CONFIG_CREATE_FAILED + ) + } + + const recordAdd = { + recordUid: options.configurationUidBytes, + clientModifiedTime: Date.now(), + data: await platform.aesGcmEncrypt(getPaddedJsonBytes(options.recordPayload), options.recordKey), + folderUid: normal64Bytes(options.folderUid), + recordKey: await platform.aesGcmEncrypt(options.recordKey, folderKey), + recordKeyEncryptedBy: Folder.FolderKeyEncryptionType.ENCRYPTED_BY_PARENT_KEY, + recordKeyType: Folder.EncryptedKeyType.encrypted_by_data_key_gcm, + } + + const response = await auth.executeRest( + addPamConfigurationV3Message({ + records: [recordAdd], + clientTime: Date.now(), + }) + ) + const status = response.records?.[0] + if (!isRecordModifySuccess(status?.status)) { + throw new KeeperSdkError( + status?.message || `Failed to create PAM Configuration in Nested Share Folder (${status?.status}).`, + ResultCodes.PAM_CONFIG_CREATE_FAILED + ) + } +} + +export async function updatePamConfigurationRecordData( + auth: Auth, + storage: InMemoryStorage, + configurationUid: string, + data: PamConfigurationTypedRecordData, + revision: number, + recordKey: Uint8Array +): Promise { + const recordPayload: Record = { + type: data.type, + title: data.title, + fields: data.fields, + custom: data.custom, + notes: data.notes, + } + const encryptedData = await platform.aesGcmEncrypt(getPaddedJsonBytes(recordPayload), recordKey) + const recordUpdate: Records.IRecordUpdate = { + recordUid: normal64Bytes(configurationUid), + clientModifiedTime: Date.now(), + revision, + data: encryptedData, + } + + const response = isNestedShareRecord(storage, configurationUid) + ? await auth.executeRest( + keeperDriveRecordsUpdate({ + records: [recordUpdate], + clientTime: Date.now(), + }) + ) + : await auth.executeRest( + recordsUpdateMessage({ + records: [recordUpdate], + clientTime: Date.now(), + }) + ) + + const status = response.records?.[0] + if (!isRecordModifySuccess(status?.status)) { + throw new KeeperSdkError( + status?.message || `Failed to update PAM Configuration (${status?.status}).`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } +} + +export async function placePamConfigurationInFolder( + auth: Auth, + storage: InMemoryStorage, + configurationUid: string, + target: PamConfigFolderTarget, + options: PlacePamConfigurationInFolderOptions = {} +): Promise { + try { + if (target.kind === 'shared_folder') { + if (options.previous?.kind === 'nsf') { + return { + success: false, + message: + 'Moving a PAM Configuration from a Nested Share Folder to a classic shared folder is not supported. Remove and recreate the configuration.', + } + } + const moveResult = await moveRecord(auth, storage, { + recordUid: configurationUid, + dstFolderUid: target.uid, + srcFolderUid: options.srcFolderUid, + canEdit: true, + }) + return { success: moveResult.success, message: moveResult.message } + } + + if (options.previous?.kind === 'shared_folder') { + return { + success: false, + message: + 'Moving a PAM Configuration from a classic shared folder to a Nested Share Folder is not supported. Create the configuration in the Nested Share Folder directly.', + } + } + + if (options.previous?.kind === 'nsf' && options.previous.uid && options.previous.uid !== target.uid) { + const unlink = await removeNestedShareRecords(storage, auth, { + records: [configurationUid], + folder: options.previous.uid, + operation: NsfRemoveOperation.Unlink, + force: true, + }) + if (!unlink.confirmed) { + return { + success: false, + message: unlink.message || 'Failed to unlink PAM Configuration from previous Nested Share Folder.', + } + } + } + + if (!isNestedShareRecord(storage, configurationUid) && !options.previous) { + return { + success: false, + message: + 'Cannot place a non-NSF PAM Configuration into a Nested Share Folder. Create it in the Nested Share Folder directly.', + } + } + + const link = await linkNestedShareRecord(storage, auth, configurationUid, target.uid) + return { success: link.success, message: link.message } + } catch (err) { + return { success: false, message: extractErrorMessage(err) } + } +} + +export async function removePamConfigurationRecord( + auth: Auth, + storage: InMemoryStorage, + configurationUid: string +): Promise { + if (isNestedShareRecord(storage, configurationUid)) { + const result = await removeNestedShareRecords(storage, auth, { + records: [configurationUid], + operation: NsfRemoveOperation.OwnerTrash, + force: true, + }) + if (!result.confirmed) { + return { + success: false, + message: result.message || `Failed to remove PAM Configuration ${configurationUid}.`, + } + } + return { success: true, message: result.message } + } + + const deleteResult = await deleteRecord(auth, storage, configurationUid) + return { success: deleteResult.success, message: deleteResult.message } +} + +export function isPamConfigurationInFolder( + storage: InMemoryStorage, + configurationUid: string, + target: PamConfigFolderTarget +): boolean { + const current = findPamConfigFolderForRecord(storage, configurationUid) + return !!current && current.kind === target.kind && current.uid === target.uid +} diff --git a/KeeperSdk/src/pam/config/removeConfig.ts b/KeeperSdk/src/pam/config/removeConfig.ts new file mode 100644 index 0000000..d51628c --- /dev/null +++ b/KeeperSdk/src/pam/config/removeConfig.ts @@ -0,0 +1,82 @@ +import type { Auth, DRecordMetadata } from '@keeper-security/keeperapi' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { isNestedShareRecord } from '../../nestedShareFolders/nsfHelpers' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { getPamConfigurationDisplayName } from './configHelpers' +import { findPamConfigurationByUidOrTitle, readTypedRecordPayload } from './configMutationHelpers' +import { removePamConfigurationRecord } from './pamConfigFolder' +import type { RemovePamConfigurationInput, RemovePamConfigurationResult } from './configTypes' + +export async function removePamConfiguration( + auth: Auth, + storage: InMemoryStorage, + input: RemovePamConfigurationInput +): Promise { + const configurationUidOrTitle = input.configurationUidOrTitle?.trim() || '' + if (!configurationUidOrTitle) { + throw new KeeperSdkError('PAM Configuration UID or title is required.', ResultCodes.PAM_CONFIG_REQUIRED) + } + + let record + try { + record = findPamConfigurationByUidOrTitle(storage, configurationUidOrTitle) + } catch (err) { + if (err instanceof KeeperSdkError && err.resultCode === ResultCodes.PAM_CONFIG_NOT_FOUND) { + return { + success: false, + found: false, + message: `PAM Configuration ${configurationUidOrTitle} not found`, + } + } + throw err + } + + const configurationUid = record.uid + const title = getPamConfigurationDisplayName(record) + const configType = readTypedRecordPayload(record).configType + + if (!isNestedShareRecord(storage, configurationUid)) { + const metadata = storage.getByUid(VaultObjectKind.Metadata, configurationUid) + if (metadata && !metadata.owner && !metadata.canEdit) { + throw new KeeperSdkError( + `Permission denied: you need edit rights to remove PAM Configuration ${configurationUid} from its shared folder.`, + ResultCodes.PAM_CONFIG_REMOVE_FAILED + ) + } + } + + try { + const deleteResult = await removePamConfigurationRecord(auth, storage, configurationUid) + if (!deleteResult.success) { + throw new KeeperSdkError( + deleteResult.message || `Failed to remove PAM Configuration ${configurationUid}.`, + ResultCodes.PAM_CONFIG_REMOVE_FAILED + ) + } + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to remove PAM Configuration: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_REMOVE_FAILED + ) + } + + return { + success: true, + found: true, + configurationUid, + title, + configType, + message: 'PAM Configuration was removed successfully.', + } +} + +export function formatRemovePamConfigurationOutput(result: RemovePamConfigurationResult): string { + if (!result.found) return result.message + const lines = [result.message] + if (result.configurationUid) lines.push(`UID: ${result.configurationUid}`) + if (result.title) lines.push(`Title: ${result.title}`) + if (result.configType) lines.push(`Type: ${result.configType}`) + return lines.join('\n') +} diff --git a/KeeperSdk/src/pam/gateway/GatewayManager.ts b/KeeperSdk/src/pam/gateway/GatewayManager.ts new file mode 100644 index 0000000..288e9b1 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/GatewayManager.ts @@ -0,0 +1,107 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { KeeperSdkError, ResultCodes } from '../../utils' +import { createGateway, formatCreateGatewayOutput } from './createGateway' +import { editGateway, formatEditGatewayOutput } from './editGateway' +import { removeGateway, formatRemoveGatewayOutput } from './removeGateway' +import { formatSetGatewayMaxInstancesOutput, setGatewayMaxInstances } from './setGatewayMaxInstances' +import { + formatGatewaysJson, + formatGatewaysOutput, + formatGatewaysTable, + listGateways, + renderGatewaysAsciiTable, +} from './listGateways' +import type { + CreateGatewayInput, + CreateGatewayResult, + EditGatewayInput, + EditGatewayResult, + FormatGatewaysTableOptions, + FormattedGatewaysTable, + ListGatewaysOptions, + ListGatewaysResult, + RemoveGatewayInput, + RemoveGatewayResult, + RenderGatewaysAsciiTableOptions, + SetGatewayMaxInstancesInput, + SetGatewayMaxInstancesResult, +} from './gatewayTypes' + +export type AuthProvider = () => Auth + +export class GatewayManager { + private readonly storage: InMemoryStorage + private readonly authProvider: AuthProvider + + constructor(storage: InMemoryStorage, authProvider: AuthProvider) { + this.storage = storage + this.authProvider = authProvider + } + + private requireAuth(): Auth { + const auth = this.authProvider() + if (!auth?.sessionToken) { + throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN) + } + return auth + } + + public async listGateways(options: ListGatewaysOptions = {}): Promise { + return listGateways(this.requireAuth(), this.storage, options) + } + + public async createGateway(input: CreateGatewayInput): Promise { + return createGateway(this.requireAuth(), this.storage, input) + } + + public formatCreateGatewayOutput(result: CreateGatewayResult): string { + return formatCreateGatewayOutput(result) + } + + public async editGateway(input: EditGatewayInput): Promise { + return editGateway(this.requireAuth(), input) + } + + public formatEditGatewayOutput(result: EditGatewayResult): string { + return formatEditGatewayOutput(result) + } + + public async removeGateway(input: RemoveGatewayInput): Promise { + return removeGateway(this.requireAuth(), input) + } + + public formatRemoveGatewayOutput(result: RemoveGatewayResult): string { + return formatRemoveGatewayOutput(result) + } + + public async setGatewayMaxInstances(input: SetGatewayMaxInstancesInput): Promise { + return setGatewayMaxInstances(this.requireAuth(), input) + } + + public formatSetGatewayMaxInstancesOutput(result: SetGatewayMaxInstancesResult): string { + return formatSetGatewayMaxInstancesOutput(result) + } + + public formatGatewaysTable( + result: ListGatewaysResult, + options: FormatGatewaysTableOptions = {} + ): FormattedGatewaysTable { + return formatGatewaysTable(result, options) + } + + public renderGatewaysAsciiTable( + table: FormattedGatewaysTable, + options: RenderGatewaysAsciiTableOptions = {} + ): string { + return renderGatewaysAsciiTable(table, options) + } + + public formatGatewaysJson(result: ListGatewaysResult, options: ListGatewaysOptions = {}): string { + return formatGatewaysJson(result, options) + } + + public formatGatewaysOutput(result: ListGatewaysResult, options: ListGatewaysOptions = {}): string { + return formatGatewaysOutput(result, options) + } +} diff --git a/KeeperSdk/src/pam/gateway/createGateway.ts b/KeeperSdk/src/pam/gateway/createGateway.ts new file mode 100644 index 0000000..6543991 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/createGateway.ts @@ -0,0 +1,231 @@ +import { createHmac, randomBytes } from 'crypto' +import type { Auth } from '@keeper-security/keeperapi' +import { + Enterprise, + addAppClientMessage, + normal64Bytes, + platform, + webSafe64FromBytes, +} from '@keeper-security/keeperapi' +import { getSecrets, initializeStorage, type KeyValueStorage } from '@keeper-security/secrets-manager-core' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { + DEFAULT_GATEWAY_TOKEN_EXPIRES_IN_MIN, + KSM_CLIENT_ID_MESSAGE, + MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN, +} from './gatewayConstants' +import { formatGatewayOneTimeToken, formatTimestampMs, resolveKsmApplication } from './gatewayHelpers' +import { + GatewayConfigInitFormat, + type CreateGatewayInput, + type CreateGatewayResult, + type GatewayConfigInitFormatInput, +} from './gatewayTypes' + +type SecretsManagerStorage = KeyValueStorage & { + getStringSync?: (key: string) => string | undefined + saveStringSync?: (key: string, value: string) => void + snapshot: () => Record +} + +function createSecretsManagerStorage(): SecretsManagerStorage { + const map = new Map() + return { + async getString(key) { + return map.get(key) + }, + async saveString(key, value) { + map.set(key, value) + }, + async getBytes(key) { + const value = map.get(key) + return value == null ? undefined : Buffer.from(value, 'base64') + }, + async saveBytes(key, value) { + map.set(key, Buffer.from(value).toString('base64')) + }, + async delete(key) { + map.delete(String(key)) + }, + getStringSync(key) { + return map.get(key) + }, + saveStringSync(key, value) { + map.set(key, value) + }, + snapshot() { + return Object.fromEntries(map) + }, + } +} + +function normalizeConfigInit(input?: GatewayConfigInitFormatInput): GatewayConfigInitFormat | undefined { + if (!input) return undefined + const value = String(input).toLowerCase() + if (value === GatewayConfigInitFormat.Json) return GatewayConfigInitFormat.Json + if (value === GatewayConfigInitFormat.B64) return GatewayConfigInitFormat.B64 + throw new KeeperSdkError( + `Invalid configInit '${input}'. Use 'json' or 'b64'.`, + ResultCodes.PAM_GATEWAY_CREATE_FAILED + ) +} + +function resolveTokenExpiresInMin(raw: number | undefined): number { + const value = raw == null ? DEFAULT_GATEWAY_TOKEN_EXPIRES_IN_MIN : Number(raw) + if (!Number.isFinite(value) || value <= 0 || value > MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN) { + throw new KeeperSdkError( + `tokenExpiresInMin must be between 1 and ${MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN} minutes.`, + ResultCodes.PAM_INVALID_TOKEN_EXPIRY + ) + } + return Math.floor(value) +} + +function snapshotValue(snapshot: Record, camel: string, upper: string, fallback = ''): string { + return snapshot[camel] || snapshot[upper] || fallback +} + +async function initKsmConfigFromToken( + oneTimeToken: string, + host: string, + format: GatewayConfigInitFormat +): Promise { + const storage = createSecretsManagerStorage() + try { + await initializeStorage(storage, oneTimeToken, host) + try { + await getSecrets({ storage }) + } catch { + // First access may fail looking up a dummy UID; config keys should still populate. + } + } catch (err) { + throw new KeeperSdkError( + `Failed to initialize KSM config: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_INIT_FAILED + ) + } + + const snapshot = storage.snapshot() + const configDict: Record = { + hostname: snapshotValue(snapshot, 'hostname', 'HOSTNAME', host), + clientId: snapshotValue(snapshot, 'clientId', 'CLIENT_ID'), + privateKey: snapshotValue(snapshot, 'privateKey', 'PRIVATE_KEY'), + serverPublicKeyId: snapshotValue(snapshot, 'serverPublicKeyId', 'SERVER_PUBLIC_KEY_ID'), + appKey: snapshotValue(snapshot, 'appKey', 'APP_KEY'), + } + const ownerPublicKey = snapshotValue(snapshot, 'ownerPublicKey', 'OWNER_PUBLIC_KEY') + if (ownerPublicKey) configDict.ownerPublicKey = ownerPublicKey + + for (const key of ['hostname', 'clientId', 'privateKey', 'serverPublicKeyId', 'appKey'] as const) { + if (!configDict[key]) { + throw new KeeperSdkError( + `Generated KSM config is invalid: "${key}" is missing or empty.`, + ResultCodes.PAM_CONFIG_INIT_FAILED + ) + } + } + + const json = JSON.stringify(configDict) + return format === GatewayConfigInitFormat.B64 ? Buffer.from(json, 'utf8').toString('base64') : json +} + +function buildCreateGatewayMessage( + appLabel: string, + gatewayName: string, + tokenExpiresInMin: number, + isInitializedConfig: boolean +): string { + if (isInitializedConfig) { + return `The one-time token was created in application [${appLabel}]. Use the initialized config in the Gateway. The new Gateway named ${gatewayName} will show up in the gateway list once it is initialized.` + } + return `The one-time token was created in application [${appLabel}]. The new Gateway named ${gatewayName} will show up in the gateway list once it is initialized. Token expires in ${tokenExpiresInMin} minutes.` +} + +export async function createGateway( + auth: Auth, + storage: InMemoryStorage, + input: CreateGatewayInput +): Promise { + const gatewayName = input.name?.trim() || '' + if (!gatewayName) { + throw new KeeperSdkError('Gateway name is required.', ResultCodes.PAM_GATEWAY_NAME_REQUIRED) + } + + const application = input.application?.trim() || '' + if (!application) { + throw new KeeperSdkError('KSM application name or UID is required.', ResultCodes.PAM_KSM_APP_REQUIRED) + } + + const tokenExpiresInMin = resolveTokenExpiresInMin(input.tokenExpiresInMin) + const configInit = normalizeConfigInit(input.configInit) + const app = await resolveKsmApplication(storage, application) + + const secretBytes = randomBytes(32) + const clientId = createHmac('sha512', secretBytes).update(KSM_CLIENT_ID_MESSAGE).digest() + const encryptedAppKey = await platform.aesGcmEncrypt(app.recordKey, secretBytes) + const firstAccessExpireOn = Date.now() + tokenExpiresInMin * 60 * 1000 + + try { + const device = await auth.executeRest( + addAppClientMessage({ + appRecordUid: normal64Bytes(app.uid), + encryptedAppKey, + clientId, + lockIp: false, + firstAccessExpireOn, + id: gatewayName, + appClientType: Enterprise.AppClientType.DISCOVERY_AND_ROTATION_CONTROLLER, + }) + ) + + const host = String(auth.options.host || '') + const oneTimeToken = formatGatewayOneTimeToken(host, secretBytes) + const deviceToken = + device.encryptedDeviceToken && device.encryptedDeviceToken.length > 0 + ? webSafe64FromBytes(device.encryptedDeviceToken) + : undefined + + const isInitializedConfig = configInit != null + const tokenOrConfig = isInitializedConfig + ? await initKsmConfigFromToken(oneTimeToken, host, configInit) + : oneTimeToken + + return { + success: true, + gatewayName, + applicationUid: app.uid, + applicationTitle: app.title, + tokenOrConfig, + isInitializedConfig, + configInit, + tokenExpiresInMin, + tokenExpiresOn: formatTimestampMs(firstAccessExpireOn), + deviceToken, + message: buildCreateGatewayMessage( + app.title || app.uid, + gatewayName, + tokenExpiresInMin, + isInitializedConfig + ), + } + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to create gateway: ${extractErrorMessage(err)}`, + ResultCodes.PAM_GATEWAY_CREATE_FAILED + ) + } +} + +export function formatCreateGatewayOutput(result: CreateGatewayResult): string { + return [ + result.message, + '', + result.isInitializedConfig ? 'Use the following initialized config in the Gateway:' : 'One-time token:', + '-----------------------------------------------', + result.tokenOrConfig, + '-----------------------------------------------', + `Token expires on: ${result.tokenExpiresOn}`, + ].join('\n') +} diff --git a/KeeperSdk/src/pam/gateway/editGateway.ts b/KeeperSdk/src/pam/gateway/editGateway.ts new file mode 100644 index 0000000..036e69a --- /dev/null +++ b/KeeperSdk/src/pam/gateway/editGateway.ts @@ -0,0 +1,135 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { modifyControllerMessage, normal64Bytes } from '@keeper-security/keeperapi' +import { EnterpriseDataInclude, EnterpriseDataManager } from '../../teams/enterpriseData' +import { applyDecryptedNodeNames, resolveParentNode } from '../../teams/teamUtils' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { + fetchEnterprisePamControllers, + requireEnterpriseGatewayByUidOrName, + toFiniteNumber, + webSafeUidFromBytes, +} from './gatewayHelpers' +import type { EditGatewayInput, EditGatewayResult } from './gatewayTypes' + +function hasNodeArgument(nodeIdOrName: EditGatewayInput['nodeIdOrName']): boolean { + return ( + nodeIdOrName !== undefined && + nodeIdOrName !== null && + !(typeof nodeIdOrName === 'string' && nodeIdOrName.trim() === '') + ) +} + +async function resolveEnterpriseNodeId(auth: Auth, nodeIdOrName: string | number): Promise { + try { + const enterpriseData = new EnterpriseDataManager(auth) + const [response, displayNames] = await Promise.all([ + enterpriseData.getData([EnterpriseDataInclude.Nodes]), + enterpriseData.getDisplayNames(), + ]) + const nodes = response.nodes || [] + applyDecryptedNodeNames(nodes, displayNames.nodes) + return resolveParentNode(nodes, nodeIdOrName).node_id + } catch (err) { + if (err instanceof KeeperSdkError) { + if (err.resultCode === ResultCodes.PARENT_NODE_NOT_FOUND) { + throw new KeeperSdkError(err.message, ResultCodes.PAM_GATEWAY_NODE_NOT_FOUND) + } + if (err.resultCode === ResultCodes.MULTIPLE_PARENT_NODE_MATCHES) { + throw new KeeperSdkError(err.message, ResultCodes.PAM_MULTIPLE_GATEWAY_NODE_MATCHES) + } + throw err + } + throw new KeeperSdkError( + `Failed to resolve enterprise node: ${extractErrorMessage(err)}`, + ResultCodes.PAM_GATEWAY_EDIT_FAILED + ) + } +} + +function buildEditResult( + partial: Omit & { unchanged?: boolean } +): EditGatewayResult { + const { unchanged, ...rest } = partial + return { + success: true, + ...rest, + message: unchanged ? `Gateway ${rest.gatewayUid} is unchanged.` : `Gateway ${rest.gatewayUid} has been edited.`, + } +} + +export async function editGateway(auth: Auth, input: EditGatewayInput): Promise { + const gatewayUidOrName = input.gatewayUidOrName?.trim() || '' + if (!gatewayUidOrName) { + throw new KeeperSdkError('Gateway UID or name is required.', ResultCodes.PAM_GATEWAY_REQUIRED) + } + + const newNameRaw = input.name == null ? '' : String(input.name).trim() + const hasName = newNameRaw.length > 0 + const hasNode = hasNodeArgument(input.nodeIdOrName) + + if (!hasName && !hasNode) { + throw new KeeperSdkError( + 'Nothing to do. At least one of name or nodeIdOrName is required.', + ResultCodes.PAM_GATEWAY_EDIT_NOTHING_TO_DO + ) + } + + const controllers = await fetchEnterprisePamControllers(auth, ResultCodes.PAM_GATEWAY_EDIT_FAILED) + const gateway = requireEnterpriseGatewayByUidOrName(controllers, gatewayUidOrName) + + const gatewayUid = webSafeUidFromBytes(gateway.controllerUid) + const previousName = gateway.controllerName || '' + const previousNodeId = toFiniteNumber(gateway.nodeId) + const gatewayName = hasName ? newNameRaw : previousName + const nodeId = hasNode ? await resolveEnterpriseNodeId(auth, input.nodeIdOrName as string | number) : previousNodeId + + const nameChanged = gatewayName !== previousName + const nodeChanged = nodeId !== previousNodeId + if (!nameChanged && !nodeChanged) { + return buildEditResult({ + gatewayUid, + previousName, + gatewayName, + previousNodeId, + nodeId, + nameChanged: false, + nodeChanged: false, + unchanged: true, + }) + } + + try { + await auth.executeRestAction( + modifyControllerMessage({ + controllerUid: normal64Bytes(gatewayUid), + controllerName: gatewayName, + nodeId, + }) + ) + } catch (err) { + throw new KeeperSdkError( + `Failed to edit gateway: ${extractErrorMessage(err)}`, + ResultCodes.PAM_GATEWAY_EDIT_FAILED + ) + } + + return buildEditResult({ + gatewayUid, + previousName, + gatewayName, + previousNodeId, + nodeId, + nameChanged, + nodeChanged, + }) +} + +export function formatEditGatewayOutput(result: EditGatewayResult): string { + return [ + result.message, + result.nameChanged + ? `Name: ${result.previousName || '(none)'} → ${result.gatewayName}` + : `Name: ${result.gatewayName}`, + result.nodeChanged ? `Node ID: ${result.previousNodeId} → ${result.nodeId}` : `Node ID: ${result.nodeId}`, + ].join('\n') +} diff --git a/KeeperSdk/src/pam/gateway/gatewayConstants.ts b/KeeperSdk/src/pam/gateway/gatewayConstants.ts new file mode 100644 index 0000000..b9588ac --- /dev/null +++ b/KeeperSdk/src/pam/gateway/gatewayConstants.ts @@ -0,0 +1,53 @@ +/** + * Keeper application (KSM) record versions recognized by this SDK. + * Add new versions here when Keeper introduces additional app-record formats. + */ +export const SUPPORTED_KSM_APP_RECORD_VERSIONS = [5] as const + +export type KsmAppRecordVersion = (typeof SUPPORTED_KSM_APP_RECORD_VERSIONS)[number] + +/** Current primary KSM application record version. Prefer SUPPORTED_KSM_APP_RECORD_VERSIONS for checks. */ +export const KSM_APP_RECORD_VERSION: KsmAppRecordVersion = SUPPORTED_KSM_APP_RECORD_VERSIONS[0] + +export const APP_NOT_ACCESSIBLE_LABEL = '[APP NOT ACCESSIBLE OR DELETED]' as const + +export const KSM_CLIENT_ID_MESSAGE = 'KEEPER_SECRETS_MANAGER_CLIENT_ID' as const + +export const DEFAULT_GATEWAY_TOKEN_EXPIRES_IN_MIN = 60 +export const MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN = 1440 + +export const MIN_GATEWAY_MAX_INSTANCES = 1 +export const MAX_GATEWAY_MAX_INSTANCES = 1000 + +export const EMPTY_GATEWAYS_MESSAGE = + 'This Enterprise does not have Gateways yet. To create a new Gateway, use `pam gateway new`. NOTE: If you have added a new Gateway, you might still need to initialize it before it is listed.' as const + +export const GATEWAY_LIST_DEFAULT_HEADERS = [ + 'KSM Application Name (UID)', + 'Gateway Name', + 'Gateway UID', + 'Status', + 'Gateway Version', +] as const + +export const GATEWAY_LIST_VERBOSE_HEADERS = [ + 'Device Name', + 'Device Token', + 'Created On', + 'Last Modified', + 'Node ID', + 'OS', + 'OS Release', + 'Machine Type', + 'OS Version', +] as const + +export const ROUTER_CONNECTION_ERROR_CODES = [ + 'ECONNREFUSED', + 'ENOTFOUND', + 'ETIMEDOUT', + 'ECONNRESET', + 'ENETUNREACH', +] as const + +export type RouterConnectionErrorCode = (typeof ROUTER_CONNECTION_ERROR_CODES)[number] diff --git a/KeeperSdk/src/pam/gateway/gatewayHelpers.ts b/KeeperSdk/src/pam/gateway/gatewayHelpers.ts new file mode 100644 index 0000000..13a0e64 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/gatewayHelpers.ts @@ -0,0 +1,276 @@ +import type { Auth, DRecord, PAM } from '@keeper-security/keeperapi' +import { getControllers, getKeeperRouterUrl, webSafe64FromBytes } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { getRecordTitle } from '../../records/RecordUtils' +import { KEEPER_PUBLIC_HOSTS, KeeperSdkError, ResultCodes, extractErrorMessage } from '../../utils' +import { + APP_NOT_ACCESSIBLE_LABEL, + ROUTER_CONNECTION_ERROR_CODES, + SUPPORTED_KSM_APP_RECORD_VERSIONS, + type RouterConnectionErrorCode, +} from './gatewayConstants' +import type { + GatewayVersionParts, + KsmApplicationDisplayInfo, + NetworkErrorLike, + ResolvedKsmApplication, +} from './gatewayTypes' + +export function getKeeperRouterBaseUrl(host: string): string { + return getKeeperRouterUrl(host, '').replace(/\/$/, '') +} + +export function webSafeUidFromBytes(bytes: Uint8Array | null | undefined): string { + if (!bytes || bytes.length === 0) return '' + return webSafe64FromBytes(bytes) +} + +export function controllerUidsEqual(a: Uint8Array | null | undefined, b: Uint8Array | null | undefined): boolean { + if (!a || !b || a.length !== b.length) return false + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true +} + +export function toFiniteNumber(value: unknown): number { + if (value == null) return 0 + if (typeof value === 'number') return Number.isFinite(value) ? value : 0 + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : 0 +} + +export function formatTimestampMs(value: unknown): string { + const ms = toFiniteNumber(value) + if (!ms) return '' + const date = new Date(ms) + if (Number.isNaN(date.getTime())) return '' + const pad = (n: number): string => String(n).padStart(2, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad( + date.getMinutes() + )}:${pad(date.getSeconds())}` +} + +export function parseGatewayVersionString(version: string | null | undefined): GatewayVersionParts { + if (!version) { + return { gatewayVersion: '', os: '', osRelease: '', machineType: '', osVersion: '' } + } + const parts = version.split(';') + return { + gatewayVersion: parts[0] || version, + os: parts[1] || '', + osRelease: parts[2] || '', + machineType: parts[3] || '', + osVersion: parts[4] || '', + } +} + +function isSupportedKsmAppRecordVersion(version: number): boolean { + return (SUPPORTED_KSM_APP_RECORD_VERSIONS as readonly number[]).includes(version) +} + +function isKsmApplicationRecord(record: DRecord): boolean { + // Version gate keeps us aligned with known Keeper app-record formats; type==='app' is the semantic check. + if (!isSupportedKsmAppRecordVersion(record.version)) return false + const data: unknown = record.data + if (!data || typeof data !== 'object') return false + return (data as { type?: unknown }).type === 'app' +} + +export function getKsmApplicationDisplayInfo( + storage: InMemoryStorage, + applicationUid: string +): KsmApplicationDisplayInfo { + if (!applicationUid) { + return { + name: null, + accessible: false, + display: `${APP_NOT_ACCESSIBLE_LABEL} ()`, + } + } + + const byUid = storage.getByUid(VaultObjectKind.Record, applicationUid) + if (byUid) { + const title = getRecordTitle(byUid) + const name = title && title !== '(untitled)' && title !== '(no data)' ? title : applicationUid + return { + name, + accessible: true, + display: `${name} (${applicationUid})`, + } + } + + return { + name: null, + accessible: false, + display: `${APP_NOT_ACCESSIBLE_LABEL} (${applicationUid})`, + } +} + +async function requireRecordKey(storage: InMemoryStorage, record: DRecord, label: string): Promise { + const recordKey = await storage.getKeyBytes(record.uid) + if (!recordKey) { + throw new KeeperSdkError( + `KSM application "${label}" key is not available. Sync the vault and try again.`, + ResultCodes.PAM_KSM_APP_NOT_FOUND + ) + } + return recordKey +} + +export async function resolveKsmApplication( + storage: InMemoryStorage, + applicationNameOrUid: string +): Promise { + const trimmed = applicationNameOrUid.trim() + if (!trimmed) { + throw new KeeperSdkError('KSM application name or UID is required.', ResultCodes.PAM_KSM_APP_REQUIRED) + } + + const byUid = storage.getByUid(VaultObjectKind.Record, trimmed) + if (byUid && isKsmApplicationRecord(byUid)) { + return { + uid: byUid.uid, + title: getRecordTitle(byUid), + record: byUid, + recordKey: await requireRecordKey(storage, byUid, trimmed), + } + } + + const lower = trimmed.toLowerCase() + const matches = storage + .getRecords() + .filter((record) => isKsmApplicationRecord(record) && getRecordTitle(record).toLowerCase() === lower) + + if (matches.length === 0) { + throw new KeeperSdkError( + `KSM Application "${trimmed}" not found. Run sync and verify the application exists in your vault.`, + ResultCodes.PAM_KSM_APP_NOT_FOUND + ) + } + if (matches.length > 1) { + throw new KeeperSdkError( + `Multiple KSM applications named "${trimmed}". Use the application UID instead.`, + ResultCodes.PAM_MULTIPLE_KSM_APP_MATCHES + ) + } + + const record = matches[0] + return { + uid: record.uid, + title: getRecordTitle(record), + record, + recordKey: await requireRecordKey(storage, record, trimmed), + } +} + +export function getKeeperRegionAbbreviation(host: string): string | null { + let normalized = host.trim().toLowerCase() + if (normalized.startsWith('http://') || normalized.startsWith('https://')) { + try { + normalized = new URL(normalized).hostname.toLowerCase() + } catch { + /* keep as-is */ + } + } + for (const [abbrev, publicHost] of Object.entries(KEEPER_PUBLIC_HOSTS)) { + if (publicHost.toLowerCase() === normalized) return abbrev + } + return null +} + +export function formatGatewayOneTimeToken(host: string, secretBytes: Uint8Array): string { + const token = webSafe64FromBytes(secretBytes) + const abbrev = getKeeperRegionAbbreviation(host) + if (abbrev) return `${abbrev}:${token}` + const bareHost = host + .replace(/^https?:\/\//i, '') + .split('/')[0] + .toLowerCase() + return `${bareHost}:${token}` +} + +export function findEnterpriseGatewayByUidOrName( + controllers: readonly PAM.IPAMController[], + gatewayUidOrName: string +): PAM.IPAMController | undefined { + const trimmed = gatewayUidOrName.trim() + if (!trimmed) return undefined + const lowered = trimmed.toLowerCase() + return controllers.find((controller) => { + const uid = webSafeUidFromBytes(controller.controllerUid) + if (uid === trimmed) return true + return (controller.controllerName || '').toLowerCase() === lowered + }) +} + +export function requireEnterpriseGatewayByUidOrName( + controllers: readonly PAM.IPAMController[], + gatewayUidOrName: string +): PAM.IPAMController { + const gateway = findEnterpriseGatewayByUidOrName(controllers, gatewayUidOrName) + if (!gateway?.controllerUid?.length) { + throw new KeeperSdkError(`Gateway "${gatewayUidOrName}" not found.`, ResultCodes.PAM_GATEWAY_NOT_FOUND) + } + return gateway +} + +export async function fetchEnterprisePamControllers( + auth: Auth, + failureResultCode: string +): Promise { + try { + const response = await auth.executeRest(getControllers()) + return response.controllers ?? [] + } catch (err) { + throw new KeeperSdkError(`Failed to list enterprise gateways: ${extractErrorMessage(err)}`, failureResultCode) + } +} + +export function groupOnlineGatewaysByControllerUid( + controllers: readonly PAM.IPAMOnlineController[] +): Map { + const map = new Map() + for (const controller of controllers) { + const uid = webSafeUidFromBytes(controller.controllerUid) + if (!uid) continue + const list = map.get(uid) + if (list) list.push(controller) + else map.set(uid, [controller]) + } + return map +} + +function isRouterConnectionErrorCode(code: string | undefined): code is RouterConnectionErrorCode { + return !!code && (ROUTER_CONNECTION_ERROR_CODES as readonly string[]).includes(code) +} + +export function isKeeperRouterConnectionError(err: unknown): boolean { + if (err == null) return false + + let code: string | undefined + let message: string | undefined + if (typeof err === 'string') { + message = err + } else if (typeof err === 'object') { + const e = err as NetworkErrorLike + code = e.code || e.errno || e.cause?.code + message = typeof e.message === 'string' ? e.message : undefined + } else { + return false + } + + if (isRouterConnectionErrorCode(code)) return true + if (!message) return false + + const msg = message.toLowerCase() + return ( + msg.includes('econnrefused') || + msg.includes('enotfound') || + msg.includes('etimedout') || + msg.includes('network') || + msg.includes('fetch failed') || + msg.includes('socket hang up') + ) +} diff --git a/KeeperSdk/src/pam/gateway/gatewayTypes.ts b/KeeperSdk/src/pam/gateway/gatewayTypes.ts new file mode 100644 index 0000000..9004ba6 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/gatewayTypes.ts @@ -0,0 +1,225 @@ +import type { DRecord } from '@keeper-security/keeperapi' + +export enum GatewayListFormat { + Table = 'table', + Json = 'json', +} + +export type GatewayListFormatInput = GatewayListFormat | `${GatewayListFormat}` + +export enum GatewayStatus { + Online = 'ONLINE', + Offline = 'OFFLINE', + Unknown = 'UNKNOWN', +} + +/** Connectivity label; pool gateways use values like `ONLINE (2 instances)`. */ +export type GatewayConnectivityStatus = GatewayStatus | `${typeof GatewayStatus.Online} (${number} instances)` + +export enum GatewayConfigInitFormat { + Json = 'json', + B64 = 'b64', +} + +export type GatewayConfigInitFormatInput = GatewayConfigInitFormat | `${GatewayConfigInitFormat}` + +export type ListGatewaysOptions = { + force?: boolean + verbose?: boolean + format?: GatewayListFormatInput + onlineOnly?: boolean +} + +export type GatewayCounts = { + online: number + offline: number + total: number +} + +export type GatewayOsMetadata = { + os: string + osRelease: string + machineType: string + osVersion: string +} + +export type GatewayVersionParts = GatewayOsMetadata & { + gatewayVersion: string +} + +export type GatewayPoolInstance = GatewayOsMetadata & { + instanceNumber: number + status: typeof GatewayStatus.Online + gatewayVersion: string + ipAddress: string + connectedOn?: number + connectedOnDisplay?: string +} + +/** One list row; pool instance rows set `isPoolInstanceRow: true`. */ +export type GatewayListRow = { + ksmApplicationName: string | null + ksmApplicationUid: string + ksmApplicationAccessible: boolean + ksmApplicationDisplay: string + gatewayName: string + gatewayUid: string + status: GatewayConnectivityStatus | string + gatewayVersion: string + deviceName?: string + deviceToken?: string + createdOn?: string + lastModified?: string + nodeId?: number + os?: string + osRelease?: string + machineType?: string + osVersion?: string + poolInstances?: GatewayPoolInstance[] + isPoolInstanceRow?: boolean + poolInstanceConnectedOnDisplay?: string +} + +export type ListGatewaysResult = { + gateways: GatewayListRow[] + routerDown: boolean + routerHost: string + gatewayCounts: GatewayCounts + aborted: boolean + message?: string +} + +export type FormattedGatewaysTable = { + headers: string[] + rows: string[][] +} + +export type FormatGatewaysTableOptions = { + verbose?: boolean +} + +export type RenderGatewaysAsciiTableOptions = { + minColWidth?: number +} + +export type KsmApplicationDisplayInfo = { + name: string | null + accessible: boolean + display: string +} + +export type ResolvedKsmApplication = { + uid: string + title: string + record: DRecord + recordKey: Uint8Array +} + +export type CreateGatewayInput = { + name: string + application: string + tokenExpiresInMin?: number + configInit?: GatewayConfigInitFormatInput +} + +export type CreateGatewayResult = { + success: boolean + gatewayName: string + applicationUid: string + applicationTitle: string | null + tokenOrConfig: string + isInitializedConfig: boolean + configInit?: GatewayConfigInitFormat + tokenExpiresInMin: number + tokenExpiresOn: string + deviceToken?: string + message: string +} + +export type EditGatewayInput = { + gatewayUidOrName: string + name?: string | null + nodeIdOrName?: string | number | null +} + +export type EditGatewayResult = { + success: boolean + gatewayUid: string + previousName: string + gatewayName: string + previousNodeId: number + nodeId: number + nameChanged: boolean + nodeChanged: boolean + message: string +} + +export type RemoveGatewayInput = { + gatewayUidOrName: string +} + +export type RemoveGatewayResult = { + success: boolean + gatewayUid: string + gatewayName: string + message: string +} + +export type SetGatewayMaxInstancesInput = { + gatewayUidOrName: string + maxInstances: number +} + +export type SetGatewayMaxInstancesResult = { + success: boolean + gatewayUid: string + gatewayName: string + maxInstances: number + message: string +} + +export type GatewayJsonPoolInstance = { + instance_number: number + status: typeof GatewayStatus.Online + gateway_version: string + ip_address: string + connected_on?: number + os?: string + os_release?: string + machine_type?: string + os_version?: string +} + +export type GatewayJsonEntry = { + ksm_app_name: string | null + ksm_app_uid: string + ksm_app_accessible: boolean + gateway_name: string + gateway_uid: string + status: string + gateway_version?: string + instances?: GatewayJsonPoolInstance[] + device_name?: string + device_token?: string + created_on?: string + last_modified?: string + node_id?: number + os?: string + os_release?: string + machine_type?: string + os_version?: string +} + +export type GatewaysJsonPayload = { + gateways: GatewayJsonEntry[] + router_host?: string + gateway_counts?: GatewayCounts + message?: string +} + +export type NetworkErrorLike = { + code?: string + errno?: string + message?: string + cause?: { code?: string } +} diff --git a/KeeperSdk/src/pam/gateway/index.ts b/KeeperSdk/src/pam/gateway/index.ts new file mode 100644 index 0000000..26df0c1 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/index.ts @@ -0,0 +1,78 @@ +export { GatewayManager } from './GatewayManager' +export type { AuthProvider } from './GatewayManager' + +export { + listGateways, + formatGatewaysTable, + renderGatewaysAsciiTable, + formatGatewaysJson, + formatGatewaysOutput, +} from './listGateways' + +export { createGateway, formatCreateGatewayOutput } from './createGateway' +export { editGateway, formatEditGatewayOutput } from './editGateway' +export { removeGateway, formatRemoveGatewayOutput } from './removeGateway' +export { setGatewayMaxInstances, formatSetGatewayMaxInstancesOutput } from './setGatewayMaxInstances' + +export { GatewayListFormat, GatewayStatus, GatewayConfigInitFormat } from './gatewayTypes' +export type { + GatewayListFormatInput, + GatewayConfigInitFormatInput, + GatewayConnectivityStatus, + ListGatewaysOptions, + GatewayCounts, + GatewayOsMetadata, + GatewayVersionParts, + GatewayPoolInstance, + GatewayListRow, + ListGatewaysResult, + FormattedGatewaysTable, + FormatGatewaysTableOptions, + RenderGatewaysAsciiTableOptions, + KsmApplicationDisplayInfo, + ResolvedKsmApplication, + CreateGatewayInput, + CreateGatewayResult, + EditGatewayInput, + EditGatewayResult, + RemoveGatewayInput, + RemoveGatewayResult, + SetGatewayMaxInstancesInput, + SetGatewayMaxInstancesResult, + GatewayJsonPoolInstance, + GatewayJsonEntry, + GatewaysJsonPayload, +} from './gatewayTypes' + +export { + KSM_APP_RECORD_VERSION, + SUPPORTED_KSM_APP_RECORD_VERSIONS, + APP_NOT_ACCESSIBLE_LABEL, + KSM_CLIENT_ID_MESSAGE, + DEFAULT_GATEWAY_TOKEN_EXPIRES_IN_MIN, + MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN, + MIN_GATEWAY_MAX_INSTANCES, + MAX_GATEWAY_MAX_INSTANCES, + EMPTY_GATEWAYS_MESSAGE, + GATEWAY_LIST_DEFAULT_HEADERS, + GATEWAY_LIST_VERBOSE_HEADERS, +} from './gatewayConstants' +export type { KsmAppRecordVersion } from './gatewayConstants' + +export { + getKeeperRouterBaseUrl, + webSafeUidFromBytes, + controllerUidsEqual, + toFiniteNumber, + formatTimestampMs, + parseGatewayVersionString, + getKsmApplicationDisplayInfo, + resolveKsmApplication, + getKeeperRegionAbbreviation, + formatGatewayOneTimeToken, + findEnterpriseGatewayByUidOrName, + requireEnterpriseGatewayByUidOrName, + fetchEnterprisePamControllers, + groupOnlineGatewaysByControllerUid, + isKeeperRouterConnectionError, +} from './gatewayHelpers' diff --git a/KeeperSdk/src/pam/gateway/listGateways.ts b/KeeperSdk/src/pam/gateway/listGateways.ts new file mode 100644 index 0000000..fdb8c60 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/listGateways.ts @@ -0,0 +1,437 @@ +import type { Auth, PAM } from '@keeper-security/keeperapi' +import { pamGetOnlineControllersMessage } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { EMPTY_GATEWAYS_MESSAGE, GATEWAY_LIST_DEFAULT_HEADERS, GATEWAY_LIST_VERBOSE_HEADERS } from './gatewayConstants' +import { + fetchEnterprisePamControllers, + formatTimestampMs, + getKeeperRouterBaseUrl, + getKsmApplicationDisplayInfo, + groupOnlineGatewaysByControllerUid, + isKeeperRouterConnectionError, + parseGatewayVersionString, + toFiniteNumber, + webSafeUidFromBytes, +} from './gatewayHelpers' +import { + GatewayListFormat, + GatewayStatus, + type FormatGatewaysTableOptions, + type FormattedGatewaysTable, + type GatewayCounts, + type GatewayJsonEntry, + type GatewayJsonPoolInstance, + type GatewayListRow, + type GatewayPoolInstance, + type GatewayVersionParts, + type GatewaysJsonPayload, + type KsmApplicationDisplayInfo, + type ListGatewaysOptions, + type ListGatewaysResult, + type RenderGatewaysAsciiTableOptions, +} from './gatewayTypes' + +const EMPTY_VERSION_PARTS = parseGatewayVersionString(undefined) +const EMPTY_GATEWAY_COUNTS: GatewayCounts = { online: 0, offline: 0, total: 0 } + +type ControllerListFields = { + controllerName: string + deviceName?: string + deviceToken?: string + created: number | null + lastModified: number | null + nodeId: number | null +} + +function emptyListResult( + partial: Pick & { + gatewayCounts?: GatewayCounts + } +): ListGatewaysResult { + return { + gateways: [], + gatewayCounts: partial.gatewayCounts ?? EMPTY_GATEWAY_COUNTS, + routerDown: partial.routerDown, + routerHost: partial.routerHost, + aborted: partial.aborted, + message: partial.message, + } +} + +function controllerListFields(controller: PAM.IPAMController): ControllerListFields { + const nodeIdRaw = controller.nodeId == null ? null : toFiniteNumber(controller.nodeId) + return { + controllerName: controller.controllerName || '', + deviceName: controller.deviceName || undefined, + deviceToken: controller.deviceToken || undefined, + created: toFiniteNumber(controller.created) || null, + lastModified: toFiniteNumber(controller.lastModified) || null, + nodeId: nodeIdRaw || null, + } +} + +async function loadOnlineControllers( + auth: Auth, + force: boolean, + routerHost: string +): Promise<{ controllers: PAM.IPAMOnlineController[]; routerDown: boolean; abort?: ListGatewaysResult }> { + try { + const response = await auth.executeRouterRest(pamGetOnlineControllersMessage()) + return { controllers: response.controllers ?? [], routerDown: false } + } catch (err) { + if (!isKeeperRouterConnectionError(err)) { + throw new KeeperSdkError( + `Unhandled error during retrieval of connected gateways: ${extractErrorMessage(err)}`, + ResultCodes.PAM_GATEWAY_LIST_FAILED + ) + } + if (!force) { + return { + controllers: [], + routerDown: true, + abort: emptyListResult({ + routerDown: true, + routerHost, + aborted: true, + message: `Looks like router is down. Use force (-f) to retrieve gateways associated with your enterprise. Router URL [${routerHost}]`, + }), + } + } + return { controllers: [], routerDown: true } + } +} + +function resolveConnectivityStatus(routerDown: boolean, connectedCount: number): GatewayListRow['status'] { + if (routerDown) return GatewayStatus.Unknown + if (connectedCount === 0) return GatewayStatus.Offline + if (connectedCount === 1) return GatewayStatus.Online + return `${GatewayStatus.Online} (${connectedCount} instances)` +} + +function toPoolInstances(connectedInstances: PAM.IPAMOnlineController[]): GatewayPoolInstance[] { + return connectedInstances.map((instance, index) => { + const versionParts = parseGatewayVersionString(instance.version) + const connectedOn = toFiniteNumber(instance.connectedOn) + return { + instanceNumber: index + 1, + status: GatewayStatus.Online, + gatewayVersion: versionParts.gatewayVersion, + ipAddress: instance.ipAddress || '', + connectedOn: connectedOn || undefined, + connectedOnDisplay: connectedOn ? formatTimestampMs(connectedOn) : '', + os: versionParts.os || undefined, + osRelease: versionParts.osRelease || undefined, + machineType: versionParts.machineType || undefined, + osVersion: versionParts.osVersion || undefined, + } + }) +} + +function toPoolInstanceRows(poolInstances: GatewayPoolInstance[]): GatewayListRow[] { + return poolInstances.map((instance) => ({ + ksmApplicationName: null, + ksmApplicationUid: '', + ksmApplicationAccessible: false, + ksmApplicationDisplay: '', + gatewayName: `|- Instance ${instance.instanceNumber} (connected: ${instance.connectedOnDisplay || ''})`, + gatewayUid: instance.ipAddress, + status: GatewayStatus.Online, + gatewayVersion: instance.gatewayVersion, + os: instance.os, + osRelease: instance.osRelease, + machineType: instance.machineType, + osVersion: instance.osVersion, + isPoolInstanceRow: true, + poolInstanceConnectedOnDisplay: instance.connectedOnDisplay, + })) +} + +function buildGatewayListRow(args: { + fields: ControllerListFields + gatewayUid: string + ksmApplicationUid: string + ksmApplication: KsmApplicationDisplayInfo + status: GatewayListRow['status'] + gatewayVersion: string + verbose: boolean + versionParts: GatewayVersionParts +}): GatewayListRow { + const { fields, ksmApplication, verbose, versionParts } = args + const row: GatewayListRow = { + ksmApplicationName: ksmApplication.name, + ksmApplicationUid: args.ksmApplicationUid, + ksmApplicationAccessible: ksmApplication.accessible, + ksmApplicationDisplay: ksmApplication.display, + gatewayName: fields.controllerName, + gatewayUid: args.gatewayUid, + status: args.status, + gatewayVersion: args.gatewayVersion, + } + + if (verbose) { + row.deviceName = fields.deviceName || '' + row.deviceToken = fields.deviceToken || '' + row.createdOn = formatTimestampMs(fields.created) + row.lastModified = formatTimestampMs(fields.lastModified) + row.nodeId = fields.nodeId == null ? undefined : fields.nodeId + row.os = versionParts.os || undefined + row.osRelease = versionParts.osRelease || undefined + row.machineType = versionParts.machineType || undefined + row.osVersion = versionParts.osVersion || undefined + } + + return row +} + +function sortGatewayListRows(gateways: GatewayListRow[]): GatewayListRow[] { + const groups: GatewayListRow[][] = [] + let current: GatewayListRow[] = [] + + for (const row of gateways) { + if (!row.isPoolInstanceRow) { + if (current.length) groups.push(current) + current = [row] + } else { + current.push(row) + } + } + if (current.length) groups.push(current) + + groups.sort((a, b) => { + const statusCmp = (a[0].status || '').localeCompare(b[0].status || '') + if (statusCmp !== 0) return statusCmp + return (a[0].ksmApplicationDisplay || '') + .toLowerCase() + .localeCompare((b[0].ksmApplicationDisplay || '').toLowerCase()) + }) + + return groups.flat() +} + +function appendOsFields( + target: { os?: string; os_release?: string; machine_type?: string; os_version?: string }, + source: { os?: string; osRelease?: string; machineType?: string; osVersion?: string } +): void { + target.os = source.os || '' + target.os_release = source.osRelease || '' + target.machine_type = source.machineType || '' + target.os_version = source.osVersion || '' +} + +export async function listGateways( + auth: Auth, + storage: InMemoryStorage, + options: ListGatewaysOptions = {} +): Promise { + const force = options.force === true + const verbose = options.verbose === true + const onlineOnly = options.onlineOnly === true + const routerHost = getKeeperRouterBaseUrl(String(auth.options.host || '')) + + const online = await loadOnlineControllers(auth, force, routerHost) + if (online.abort) return online.abort + + const enterpriseControllers = await fetchEnterprisePamControllers(auth, ResultCodes.PAM_GATEWAY_LIST_FAILED) + if (!enterpriseControllers.length) { + return emptyListResult({ + routerDown: online.routerDown, + routerHost, + aborted: false, + message: EMPTY_GATEWAYS_MESSAGE, + }) + } + + const connectedByUid = groupOnlineGatewaysByControllerUid(online.controllers) + const gatewayCounts: GatewayCounts = { + online: 0, + offline: 0, + total: enterpriseControllers.length, + } + const gateways: GatewayListRow[] = [] + + for (const controller of enterpriseControllers) { + const gatewayUid = webSafeUidFromBytes(controller.controllerUid) + const connectedInstances = connectedByUid.get(gatewayUid) ?? [] + const isOnline = !online.routerDown && connectedInstances.length > 0 + + if (!online.routerDown) { + if (isOnline) gatewayCounts.online += 1 + else gatewayCounts.offline += 1 + } + + if (onlineOnly && !isOnline) continue + + const ksmApplicationUid = webSafeUidFromBytes(controller.applicationUid) + const ksmApplication = getKsmApplicationDisplayInfo(storage, ksmApplicationUid) + const fields = controllerListFields(controller) + const status = resolveConnectivityStatus(online.routerDown, connectedInstances.length) + const isPool = connectedInstances.length > 1 + + if (!isPool) { + const versionParts = parseGatewayVersionString(connectedInstances[0]?.version) + gateways.push( + buildGatewayListRow({ + fields, + gatewayUid, + ksmApplicationUid, + ksmApplication, + status, + gatewayVersion: versionParts.gatewayVersion, + verbose, + versionParts, + }) + ) + continue + } + + const poolInstances = toPoolInstances(connectedInstances) + const parent = buildGatewayListRow({ + fields, + gatewayUid, + ksmApplicationUid, + ksmApplication, + status, + gatewayVersion: '', + verbose, + versionParts: EMPTY_VERSION_PARTS, + }) + parent.poolInstances = poolInstances + gateways.push(parent, ...toPoolInstanceRows(poolInstances)) + } + + return { + gateways: sortGatewayListRows(gateways), + routerDown: online.routerDown, + routerHost, + gatewayCounts, + aborted: false, + } +} + +export function formatGatewaysTable( + result: ListGatewaysResult, + options: FormatGatewaysTableOptions = {} +): FormattedGatewaysTable { + const verbose = options.verbose === true + const headers: string[] = [...GATEWAY_LIST_DEFAULT_HEADERS] + if (verbose) headers.push(...GATEWAY_LIST_VERBOSE_HEADERS) + + const rows = result.gateways.map((gateway) => { + const isInstance = gateway.isPoolInstanceRow === true + const row: string[] = [ + isInstance ? '' : gateway.ksmApplicationDisplay, + gateway.gatewayName, + gateway.gatewayUid, + gateway.status, + gateway.gatewayVersion, + ] + if (verbose) { + row.push( + isInstance ? '' : gateway.deviceName || '', + isInstance ? '' : gateway.deviceToken || '', + isInstance ? gateway.poolInstanceConnectedOnDisplay || '' : gateway.createdOn || '', + isInstance ? '' : gateway.lastModified || '', + isInstance ? '' : gateway.nodeId != null ? String(gateway.nodeId) : '', + gateway.os || '', + gateway.osRelease || '', + gateway.machineType || '', + gateway.osVersion || '' + ) + } + return row + }) + + return { headers, rows } +} + +export function renderGatewaysAsciiTable( + table: FormattedGatewaysTable, + options: RenderGatewaysAsciiTableOptions = {} +): string { + const minColWidth = options.minColWidth ?? 2 + const widths = table.headers.map((header, col) => { + let width = Math.max(header.length, minColWidth) + for (const row of table.rows) { + width = Math.max(width, (row[col] || '').length) + } + return width + }) + + const formatRow = (cells: string[]): string => cells.map((cell, i) => (cell || '').padEnd(widths[i])).join(' ') + + return [ + formatRow([...table.headers]), + widths.map((w) => '-'.repeat(w)).join(' '), + ...table.rows.map(formatRow), + ].join('\n') +} + +export function formatGatewaysJson(result: ListGatewaysResult, options: ListGatewaysOptions = {}): string { + const verbose = options.verbose === true + const onlineOnly = options.onlineOnly === true + + const gateways: GatewayJsonEntry[] = result.gateways + .filter((g) => !g.isPoolInstanceRow) + .map((g) => { + const entry: GatewayJsonEntry = { + ksm_app_name: g.ksmApplicationName, + ksm_app_uid: g.ksmApplicationUid, + ksm_app_accessible: g.ksmApplicationAccessible, + gateway_name: g.gatewayName, + gateway_uid: g.gatewayUid, + status: g.status, + } + + if (g.poolInstances?.length) { + entry.instances = g.poolInstances.map((instance): GatewayJsonPoolInstance => { + const inst: GatewayJsonPoolInstance = { + instance_number: instance.instanceNumber, + status: instance.status, + gateway_version: instance.gatewayVersion, + ip_address: instance.ipAddress, + connected_on: instance.connectedOn, + } + if (verbose) appendOsFields(inst, instance) + return inst + }) + } else { + entry.gateway_version = g.gatewayVersion + } + + if (verbose) { + entry.device_name = g.deviceName || '' + entry.device_token = g.deviceToken || '' + entry.created_on = g.createdOn || '' + entry.last_modified = g.lastModified || '' + entry.node_id = g.nodeId + if (!g.poolInstances?.length) appendOsFields(entry, g) + } + return entry + }) + + const payload: GatewaysJsonPayload = { gateways } + if (verbose) payload.router_host = result.routerHost + if (onlineOnly) payload.gateway_counts = result.gatewayCounts + if (result.message) payload.message = result.message + + return JSON.stringify(payload, null, 2) +} + +export function formatGatewaysOutput(result: ListGatewaysResult, options: ListGatewaysOptions = {}): string { + const format = String(options.format || GatewayListFormat.Table).toLowerCase() + if (format === GatewayListFormat.Json) return formatGatewaysJson(result, options) + + const parts: string[] = [] + if (options.verbose) parts.push(`Router Host: ${result.routerHost}`, '') + if (result.message && result.gateways.length === 0) { + parts.push(result.message) + return parts.join('\n') + } + parts.push(renderGatewaysAsciiTable(formatGatewaysTable(result, { verbose: options.verbose }))) + if (options.onlineOnly) { + const { online, offline, total } = result.gatewayCounts + parts.push('', `Gateways: Online: ${online}, Offline: ${offline}, Total: ${total}`) + } + return parts.join('\n') +} diff --git a/KeeperSdk/src/pam/gateway/removeGateway.ts b/KeeperSdk/src/pam/gateway/removeGateway.ts new file mode 100644 index 0000000..9092a7e --- /dev/null +++ b/KeeperSdk/src/pam/gateway/removeGateway.ts @@ -0,0 +1,55 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { removeControllerMessage } from '@keeper-security/keeperapi' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { + controllerUidsEqual, + fetchEnterprisePamControllers, + requireEnterpriseGatewayByUidOrName, + webSafeUidFromBytes, +} from './gatewayHelpers' +import type { RemoveGatewayInput, RemoveGatewayResult } from './gatewayTypes' + +export async function removeGateway(auth: Auth, input: RemoveGatewayInput): Promise { + const gatewayUidOrName = input.gatewayUidOrName?.trim() || '' + if (!gatewayUidOrName) { + throw new KeeperSdkError('Gateway UID or name is required.', ResultCodes.PAM_GATEWAY_REQUIRED) + } + + const controllers = await fetchEnterprisePamControllers(auth, ResultCodes.PAM_GATEWAY_REMOVE_FAILED) + const gateway = requireEnterpriseGatewayByUidOrName(controllers, gatewayUidOrName) + const gatewayUid = webSafeUidFromBytes(gateway.controllerUid) + const gatewayName = gateway.controllerName || gatewayUid + const controllerUidBytes = gateway.controllerUid + + try { + const response = await auth.executeRest(removeControllerMessage({ uid: controllerUidBytes })) + // PAMRemoveControllerResponse.controllers holds per-controller failure details. + // Empty list / matching entry without a message ⇒ success; non-empty message ⇒ failure. + const failureEntry = (response.controllers ?? []).find( + (entry) => + controllerUidsEqual(entry.controllerUid, controllerUidBytes) && + typeof entry.message === 'string' && + entry.message.trim().length > 0 + ) + if (failureEntry) { + throw new KeeperSdkError(failureEntry.message.trim(), ResultCodes.PAM_GATEWAY_REMOVE_FAILED) + } + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to remove gateway: ${extractErrorMessage(err)}`, + ResultCodes.PAM_GATEWAY_REMOVE_FAILED + ) + } + + return { + success: true, + gatewayUid, + gatewayName, + message: `Gateway ${gatewayName} has been removed.`, + } +} + +export function formatRemoveGatewayOutput(result: RemoveGatewayResult): string { + return result.message +} diff --git a/KeeperSdk/src/pam/gateway/setGatewayMaxInstances.ts b/KeeperSdk/src/pam/gateway/setGatewayMaxInstances.ts new file mode 100644 index 0000000..ddd6139 --- /dev/null +++ b/KeeperSdk/src/pam/gateway/setGatewayMaxInstances.ts @@ -0,0 +1,70 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { setControllerMaxInstanceCountMessage } from '@keeper-security/keeperapi' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { MAX_GATEWAY_MAX_INSTANCES, MIN_GATEWAY_MAX_INSTANCES } from './gatewayConstants' +import { + fetchEnterprisePamControllers, + requireEnterpriseGatewayByUidOrName, + webSafeUidFromBytes, +} from './gatewayHelpers' +import type { SetGatewayMaxInstancesInput, SetGatewayMaxInstancesResult } from './gatewayTypes' + +function resolveMaxInstances(raw: number): number { + if (typeof raw !== 'number' || !Number.isFinite(raw) || !Number.isInteger(raw)) { + throw new KeeperSdkError( + `maxInstances must be an integer between ${MIN_GATEWAY_MAX_INSTANCES} and ${MAX_GATEWAY_MAX_INSTANCES}.`, + ResultCodes.PAM_GATEWAY_INVALID_MAX_INSTANCES + ) + } + if (raw < MIN_GATEWAY_MAX_INSTANCES || raw > MAX_GATEWAY_MAX_INSTANCES) { + throw new KeeperSdkError( + `maxInstances must be an integer between ${MIN_GATEWAY_MAX_INSTANCES} and ${MAX_GATEWAY_MAX_INSTANCES}.`, + ResultCodes.PAM_GATEWAY_INVALID_MAX_INSTANCES + ) + } + return raw +} + +export async function setGatewayMaxInstances( + auth: Auth, + input: SetGatewayMaxInstancesInput +): Promise { + const gatewayUidOrName = input.gatewayUidOrName?.trim() || '' + if (!gatewayUidOrName) { + throw new KeeperSdkError('Gateway UID or name is required.', ResultCodes.PAM_GATEWAY_REQUIRED) + } + + const maxInstanceCount = resolveMaxInstances(input.maxInstances) + + const controllers = await fetchEnterprisePamControllers(auth, ResultCodes.PAM_GATEWAY_SET_MAX_INSTANCES_FAILED) + const gateway = requireEnterpriseGatewayByUidOrName(controllers, gatewayUidOrName) + const gatewayUid = webSafeUidFromBytes(gateway.controllerUid) + const gatewayName = gateway.controllerName || gatewayUid + + try { + await auth.executeRestAction( + setControllerMaxInstanceCountMessage({ + controllerUid: gateway.controllerUid, + maxInstanceCount, + }) + ) + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to set max instances: ${extractErrorMessage(err)}`, + ResultCodes.PAM_GATEWAY_SET_MAX_INSTANCES_FAILED + ) + } + + return { + success: true, + gatewayUid, + gatewayName, + maxInstances: maxInstanceCount, + message: `${gatewayName}: max instance count set to ${maxInstanceCount}`, + } +} + +export function formatSetGatewayMaxInstancesOutput(result: SetGatewayMaxInstancesResult): string { + return result.message +} diff --git a/KeeperSdk/src/pam/index.ts b/KeeperSdk/src/pam/index.ts new file mode 100644 index 0000000..fc54f4b --- /dev/null +++ b/KeeperSdk/src/pam/index.ts @@ -0,0 +1,173 @@ +export { PamManager } from './PamManager' +export type { AuthProvider as PamAuthProvider } from './PamManager' + +export { + GatewayManager, + listGateways, + formatGatewaysTable, + renderGatewaysAsciiTable, + formatGatewaysJson, + formatGatewaysOutput, + createGateway, + formatCreateGatewayOutput, + editGateway, + formatEditGatewayOutput, + removeGateway, + formatRemoveGatewayOutput, + setGatewayMaxInstances, + formatSetGatewayMaxInstancesOutput, + GatewayListFormat, + GatewayStatus, + GatewayConfigInitFormat, + KSM_APP_RECORD_VERSION, + SUPPORTED_KSM_APP_RECORD_VERSIONS, + APP_NOT_ACCESSIBLE_LABEL, + KSM_CLIENT_ID_MESSAGE, + DEFAULT_GATEWAY_TOKEN_EXPIRES_IN_MIN, + MAX_GATEWAY_TOKEN_EXPIRES_IN_MIN, + MIN_GATEWAY_MAX_INSTANCES, + MAX_GATEWAY_MAX_INSTANCES, + EMPTY_GATEWAYS_MESSAGE, + GATEWAY_LIST_DEFAULT_HEADERS, + GATEWAY_LIST_VERBOSE_HEADERS, + getKeeperRouterBaseUrl, + webSafeUidFromBytes, + controllerUidsEqual, + toFiniteNumber, + formatTimestampMs, + parseGatewayVersionString, + getKsmApplicationDisplayInfo, + resolveKsmApplication, + getKeeperRegionAbbreviation, + formatGatewayOneTimeToken, + findEnterpriseGatewayByUidOrName, + requireEnterpriseGatewayByUidOrName, + fetchEnterprisePamControllers, + groupOnlineGatewaysByControllerUid, + isKeeperRouterConnectionError, +} from './gateway' +export type { + AuthProvider as GatewayAuthProvider, + GatewayListFormatInput, + GatewayConfigInitFormatInput, + GatewayConnectivityStatus, + ListGatewaysOptions, + GatewayCounts, + GatewayOsMetadata, + GatewayVersionParts, + GatewayPoolInstance, + GatewayListRow, + ListGatewaysResult, + FormattedGatewaysTable, + FormatGatewaysTableOptions, + RenderGatewaysAsciiTableOptions, + KsmApplicationDisplayInfo, + ResolvedKsmApplication, + CreateGatewayInput, + CreateGatewayResult, + EditGatewayInput, + EditGatewayResult, + RemoveGatewayInput, + RemoveGatewayResult, + SetGatewayMaxInstancesInput, + SetGatewayMaxInstancesResult, + GatewayJsonPoolInstance, + GatewayJsonEntry, + GatewaysJsonPayload, + KsmAppRecordVersion, +} from './gateway' + +export { + ConfigManager, + listPamConfigurations, + formatPamConfigurationsTable, + renderPamConfigurationsAsciiTable, + formatPamConfigurationsJson, + formatPamConfigurationsOutput, + createPamConfiguration, + formatCreatePamConfigurationOutput, + editPamConfiguration, + formatEditPamConfigurationOutput, + removePamConfiguration, + formatRemovePamConfigurationOutput, + PamConfigListFormat, + PAM_CONFIGURATION_RECORD_VERSION, + PAM_CONFIGURATION_RECORD_TYPES, + PAM_CONFIG_ENVIRONMENT_TO_RECORD_TYPE, + PAM_CONFIG_ENVIRONMENTS, + PAM_RESOURCES_FIELD_TYPE, + FILE_REF_FIELD_TYPE, + SCHEDULE_FIELD_TYPE, + DEFAULT_PAM_CONFIG_SCHEDULE_VALUE, + EMPTY_PAM_CONFIGURATIONS_MESSAGE, + PAM_CONFIG_LIST_DEFAULT_HEADERS, + PAM_CONFIG_LIST_VERBOSE_HEADERS, + PAM_CONFIG_DETAIL_HEADERS, + PAM_CONFIG_DETAIL_LABELS, + PAM_CONFIG_PERMISSION_DAG_KEYS, + PAM_CONFIG_PERMISSION_FLAGS, + PAM_CONFIG_PERMISSION_VALUES, + isPamConfigurationRecordType, + isPamConfigEnvironment, + resolvePamConfigurationRecordType, + isPamConfigurationRecord, + getPamConfigurationFields, + parsePamResources, + resolveSharedFolderName, + findSharedFolderUidForRecord, + listPamConfigurationRecords, + getPamConfigurationDisplayName, + normalizeFields, + ensureScheduleField, + mergeRecordFields, + readTypedRecordPayload, + upsertPamResourcesField, + resolveSharedFolderUid, + resolveGatewayUidSoft, + findPamConfigurationByUidOrTitle, + resolveResourceRecordUidsToRemove, + linkConfigurationController, + moveConfigurationToSharedFolder, + hasPermissionsInput, + convertPermissionValue, + normalizePermissionValue, + buildAllowedSettingsFromPermissions, + applyPamConfigurationPermissions, + isPamConfigurationInFolder, + resolvePamConfigFolder, + findPamConfigFolderForRecord, + resolvePamConfigFolderTargetFromUid, + resolvePamConfigFolderName, + formatPamConfigFolderDisplay, + placePamConfigurationInFolder, +} from './config' +export type { + PamConfigurationRecordType, + PamConfigEnvironment, + PamConfigPermissionFlag, + PamConfigListFormatInput, + ListPamConfigurationsOptions, + PamResourcesInfo, + PamConfigurationField, + PamConfigurationListRow, + PamConfigurationDetail, + ListPamConfigurationsResult, + FormattedPamConfigurationsTable, + FormatPamConfigurationsTableOptions, + RenderPamConfigurationsAsciiTableOptions, + PamConfigurationJsonField, + PamConfigurationJsonEntry, + PamConfigurationsJsonPayload, + PamConfigurationRecordFieldInput, + PamConfigurationPermissionValue, + PamConfigurationPermissionsInput, + PamNetworkAllowedSettings, + PamConfigFolderKind, + PamConfigFolderTarget, + CreatePamConfigurationInput, + CreatePamConfigurationResult, + EditPamConfigurationInput, + EditPamConfigurationResult, + RemovePamConfigurationInput, + RemovePamConfigurationResult, +} from './config' diff --git a/KeeperSdk/src/records/RecordOperations.ts b/KeeperSdk/src/records/RecordOperations.ts index 9d74471..b7bb802 100644 --- a/KeeperSdk/src/records/RecordOperations.ts +++ b/KeeperSdk/src/records/RecordOperations.ts @@ -272,14 +272,27 @@ export async function updateRecord( } } -export async function deleteRecord(auth: Auth, recordUid: string): Promise { +function toPreDeleteFromType(folderType: FolderKind): RecordPreDeleteObject['from_type'] { + if (folderType === FolderKind.UserFolder) return FolderKind.UserFolder + return FolderKind.SharedFolderFolder +} + +export async function deleteRecord( + auth: Auth, + storage: InMemoryStorage, + recordUid: string +): Promise { + const srcUid = await findRecordSourceFolder(recordUid, storage) + const src = resolveFolder(srcUid, storage) + const fromType = toPreDeleteFromType(src.folderType) + const preDeleteRequest = { objects: [ { object_uid: recordUid, object_type: VaultObjectKind.Record, - from_uid: '', - from_type: FolderKind.UserFolder, + from_uid: src.uid || '', + from_type: fromType, delete_resolution: DeleteResolution.Unlink, } as RecordPreDeleteObject, ], @@ -290,7 +303,11 @@ export async function deleteRecord(auth: Auth, recordUid: string): Promise { + // Root user-folder records are linked under the empty folder UID. + const rootDependencies = (await storage.getDependencies('')) || [] + if ( + rootDependencies.some( + (dependency) => dependency.kind === VaultObjectKind.Record && dependency.uid === recordUid + ) + ) { + return '' + } + const folderKinds = [FolderKind.UserFolder, FolderKind.SharedFolder, FolderKind.SharedFolderFolder] as const for (const kind of folderKinds) { @@ -444,7 +471,7 @@ async function findRecordSourceFolder(recordUid: string, storage: InMemoryStorag const sharedFolderRecord = storage .getAll(VaultObjectKind.SharedFolderRecord) .find((candidate) => candidate.recordUid === recordUid) - return sharedFolderRecord ? sharedFolderRecord.sharedFolderUid : '' + return sharedFolderRecord?.sharedFolderUid || '' } export async function moveRecord( @@ -466,9 +493,9 @@ export async function moveRecord( cascade: false, from_type: src.folderType, from_uid: src.uid || undefined, - can_edit: canEdit, - can_reshare: canShare, } + if (canEdit) moveObj.can_edit = true + if (canShare) moveObj.can_reshare = true const transitionKeys: TransitionKeyObject[] = [] diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index 524b6f0..a651cdb 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -142,6 +142,42 @@ export enum UserErrorCode { TeamUserRemoveFailed = 'team_user_remove_failed', } +export enum PamErrorCode { + RouterUnavailable = 'pam_router_unavailable', + GatewayListFailed = 'pam_gateway_list_failed', + GatewayCreateFailed = 'pam_gateway_create_failed', + GatewayNameRequired = 'pam_gateway_name_required', + KsmAppRequired = 'pam_ksm_app_required', + KsmAppNotFound = 'pam_ksm_app_not_found', + MultipleKsmAppMatches = 'pam_multiple_ksm_app_matches', + InvalidTokenExpiry = 'pam_invalid_token_expiry', + ConfigInitFailed = 'pam_config_init_failed', + ConfigInitUnavailable = 'pam_config_init_unavailable', + GatewayRequired = 'pam_gateway_required', + GatewayNotFound = 'pam_gateway_not_found', + GatewayEditNothingToDo = 'pam_gateway_edit_nothing_to_do', + GatewayEditFailed = 'pam_gateway_edit_failed', + GatewayNodeNotFound = 'pam_gateway_node_not_found', + MultipleGatewayNodeMatches = 'pam_multiple_gateway_node_matches', + GatewayRemoveFailed = 'pam_gateway_remove_failed', + GatewayInvalidMaxInstances = 'pam_gateway_invalid_max_instances', + GatewaySetMaxInstancesFailed = 'pam_gateway_set_max_instances_failed', + ConfigNotFound = 'pam_config_not_found', + ConfigInvalid = 'pam_config_invalid', + ConfigTitleRequired = 'pam_config_title_required', + ConfigTypeRequired = 'pam_config_type_required', + ConfigTypeInvalid = 'pam_config_type_invalid', + ConfigSharedFolderRequired = 'pam_config_shared_folder_required', + ConfigSharedFolderNotFound = 'pam_config_shared_folder_not_found', + ConfigCreateFailed = 'pam_config_create_failed', + ConfigMoveFailed = 'pam_config_move_failed', + ConfigRequired = 'pam_config_required', + ConfigEditNothingToDo = 'pam_config_edit_nothing_to_do', + ConfigEditFailed = 'pam_config_edit_failed', + MultipleConfigMatches = 'pam_multiple_config_matches', + ConfigRemoveFailed = 'pam_config_remove_failed', +} + export const ResultCodes = { INVALID_CREDENTIALS: AuthErrorCode.InvalidCredentials, MISSING_USERNAME: AuthErrorCode.MissingUsername, @@ -221,6 +257,39 @@ export const ResultCodes = { NO_TEAMS_FOR_USER_OP: UserErrorCode.NoTeamsForUserOp, TEAM_USER_ADD_FAILED: UserErrorCode.TeamUserAddFailed, TEAM_USER_REMOVE_FAILED: UserErrorCode.TeamUserRemoveFailed, + PAM_ROUTER_UNAVAILABLE: PamErrorCode.RouterUnavailable, + PAM_GATEWAY_LIST_FAILED: PamErrorCode.GatewayListFailed, + PAM_GATEWAY_CREATE_FAILED: PamErrorCode.GatewayCreateFailed, + PAM_GATEWAY_NAME_REQUIRED: PamErrorCode.GatewayNameRequired, + PAM_KSM_APP_REQUIRED: PamErrorCode.KsmAppRequired, + PAM_KSM_APP_NOT_FOUND: PamErrorCode.KsmAppNotFound, + PAM_MULTIPLE_KSM_APP_MATCHES: PamErrorCode.MultipleKsmAppMatches, + PAM_INVALID_TOKEN_EXPIRY: PamErrorCode.InvalidTokenExpiry, + PAM_CONFIG_INIT_FAILED: PamErrorCode.ConfigInitFailed, + PAM_CONFIG_INIT_UNAVAILABLE: PamErrorCode.ConfigInitUnavailable, + PAM_GATEWAY_REQUIRED: PamErrorCode.GatewayRequired, + PAM_GATEWAY_NOT_FOUND: PamErrorCode.GatewayNotFound, + PAM_GATEWAY_EDIT_NOTHING_TO_DO: PamErrorCode.GatewayEditNothingToDo, + PAM_GATEWAY_EDIT_FAILED: PamErrorCode.GatewayEditFailed, + PAM_GATEWAY_NODE_NOT_FOUND: PamErrorCode.GatewayNodeNotFound, + PAM_MULTIPLE_GATEWAY_NODE_MATCHES: PamErrorCode.MultipleGatewayNodeMatches, + PAM_GATEWAY_REMOVE_FAILED: PamErrorCode.GatewayRemoveFailed, + PAM_GATEWAY_INVALID_MAX_INSTANCES: PamErrorCode.GatewayInvalidMaxInstances, + PAM_GATEWAY_SET_MAX_INSTANCES_FAILED: PamErrorCode.GatewaySetMaxInstancesFailed, + PAM_CONFIG_NOT_FOUND: PamErrorCode.ConfigNotFound, + PAM_CONFIG_INVALID: PamErrorCode.ConfigInvalid, + PAM_CONFIG_TITLE_REQUIRED: PamErrorCode.ConfigTitleRequired, + PAM_CONFIG_TYPE_REQUIRED: PamErrorCode.ConfigTypeRequired, + PAM_CONFIG_TYPE_INVALID: PamErrorCode.ConfigTypeInvalid, + PAM_CONFIG_SHARED_FOLDER_REQUIRED: PamErrorCode.ConfigSharedFolderRequired, + PAM_CONFIG_SHARED_FOLDER_NOT_FOUND: PamErrorCode.ConfigSharedFolderNotFound, + PAM_CONFIG_CREATE_FAILED: PamErrorCode.ConfigCreateFailed, + PAM_CONFIG_MOVE_FAILED: PamErrorCode.ConfigMoveFailed, + PAM_CONFIG_REQUIRED: PamErrorCode.ConfigRequired, + PAM_CONFIG_EDIT_NOTHING_TO_DO: PamErrorCode.ConfigEditNothingToDo, + PAM_CONFIG_EDIT_FAILED: PamErrorCode.ConfigEditFailed, + PAM_MULTIPLE_CONFIG_MATCHES: PamErrorCode.MultipleConfigMatches, + PAM_CONFIG_REMOVE_FAILED: PamErrorCode.ConfigRemoveFailed, AUDIT_INVALID_REPORT_TYPE: AuditReportErrorCode.InvalidReportType, AUDIT_INVALID_CREATED_FILTER: AuditReportErrorCode.InvalidCreatedFilter, AUDIT_INVALID_FILTER: AuditReportErrorCode.InvalidFilter, diff --git a/KeeperSdk/src/utils/index.ts b/KeeperSdk/src/utils/index.ts index 65ea9f2..ee7a2b3 100644 --- a/KeeperSdk/src/utils/index.ts +++ b/KeeperSdk/src/utils/index.ts @@ -12,6 +12,7 @@ export { ActionReportErrorCode, PasswordReportErrorCode, NsfErrorCode, + PamErrorCode, KEEPER_PUBLIC_HOSTS, } from './constants' export { Logger, ConsoleLogger, LogLevel, logger, setLogger, getLogger, resetLogger, writeOutput } from './Logger' diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 7f8f81a..fc5c8ef 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -86,6 +86,7 @@ import { } from '../enterpriseReport' import { UserManager } from '../users/UserManager' import { NestedShareFolderManager } from '../nestedShareFolders/NestedShareFolderManager' +import { PamManager } from '../pam/PamManager' import { isNestedShareFolder } from '../nestedShareFolders/nsfHelpers' import { formatListNsfTable, renderListNsfAsciiTable, formatListNsfOutput } from '../nestedShareFolders/listNsf' import { formatNsfDetail } from '../nestedShareFolders/getNsf' @@ -142,6 +143,34 @@ import type { UpdateNsfRecordResult, UpdateNsfRecordResultItem, } from '../nestedShareFolders/nsfTypes' +import type { + ListGatewaysOptions, + ListGatewaysResult, + FormattedGatewaysTable, + FormatGatewaysTableOptions, + RenderGatewaysAsciiTableOptions, + CreateGatewayInput, + CreateGatewayResult, + EditGatewayInput, + EditGatewayResult, + RemoveGatewayInput, + RemoveGatewayResult, + SetGatewayMaxInstancesInput, + SetGatewayMaxInstancesResult, +} from '../pam/gateway/gatewayTypes' +import type { + FormatPamConfigurationsTableOptions, + FormattedPamConfigurationsTable, + ListPamConfigurationsOptions, + ListPamConfigurationsResult, + RenderPamConfigurationsAsciiTableOptions, + CreatePamConfigurationInput, + CreatePamConfigurationResult, + EditPamConfigurationInput, + EditPamConfigurationResult, + RemovePamConfigurationInput, + RemovePamConfigurationResult, +} from '../pam/config/configTypes' import type { ListUserRow, ListUsersOptions, @@ -211,6 +240,7 @@ export class KeeperVault { private readonly enterpriseReportManager: EnterpriseReportManager private readonly userManager: UserManager private readonly nestedShareFolderManager: NestedShareFolderManager + private readonly pamManager: PamManager constructor(config?: KeeperVaultConfig) { this.config = { @@ -236,12 +266,25 @@ export class KeeperVault { this.enterpriseReportManager = new EnterpriseReportManager(authProvider) this.userManager = new UserManager(authProvider) this.nestedShareFolderManager = new NestedShareFolderManager(this.storage, authProvider) + this.pamManager = new PamManager(this.storage, authProvider) } public getNestedShareFolderManager(): NestedShareFolderManager { return this.nestedShareFolderManager } + public getPamManager(): PamManager { + return this.pamManager + } + + public getGatewayManager() { + return this.pamManager.getGatewayManager() + } + + public getConfigManager() { + return this.pamManager.getConfigManager() + } + public getFolderManager(): FolderManager { return this.folderManager } @@ -740,7 +783,7 @@ export class KeeperVault { public async deleteRecord(recordUid: string): Promise { const auth = this.getAuthOrThrow() - const result = await deleteRecordOp(auth, recordUid) + const result = await deleteRecordOp(auth, this.storage, recordUid) if (result.success) await this.syncIfNeeded() return result } @@ -979,6 +1022,128 @@ export class KeeperVault { return formatNsfRecordPermissionFailures(failures, kind) } + public async listGateways(options?: ListGatewaysOptions): Promise { + return this.pamManager.listGateways(options ?? {}) + } + + public async createGateway(input: CreateGatewayInput): Promise { + return this.pamManager.createGateway(input) + } + + public formatCreateGatewayOutput(result: CreateGatewayResult): string { + return this.pamManager.formatCreateGatewayOutput(result) + } + + public async editGateway(input: EditGatewayInput): Promise { + return this.pamManager.editGateway(input) + } + + public formatEditGatewayOutput(result: EditGatewayResult): string { + return this.pamManager.formatEditGatewayOutput(result) + } + + public async removeGateway(input: RemoveGatewayInput): Promise { + return this.pamManager.removeGateway(input) + } + + public formatRemoveGatewayOutput(result: RemoveGatewayResult): string { + return this.pamManager.formatRemoveGatewayOutput(result) + } + + public async setGatewayMaxInstances(input: SetGatewayMaxInstancesInput): Promise { + return this.pamManager.setGatewayMaxInstances(input) + } + + public formatSetGatewayMaxInstancesOutput(result: SetGatewayMaxInstancesResult): string { + return this.pamManager.formatSetGatewayMaxInstancesOutput(result) + } + + public formatGatewaysTable( + result: ListGatewaysResult, + options?: FormatGatewaysTableOptions + ): FormattedGatewaysTable { + return this.pamManager.formatGatewaysTable(result, options ?? {}) + } + + public renderGatewaysAsciiTable(table: FormattedGatewaysTable, options?: RenderGatewaysAsciiTableOptions): string { + return this.pamManager.renderGatewaysAsciiTable(table, options ?? {}) + } + + public formatGatewaysJson(result: ListGatewaysResult, options?: ListGatewaysOptions): string { + return this.pamManager.formatGatewaysJson(result, options ?? {}) + } + + public formatGatewaysOutput(result: ListGatewaysResult, options?: ListGatewaysOptions): string { + return this.pamManager.formatGatewaysOutput(result, options ?? {}) + } + + public listPamConfigurations(options?: ListPamConfigurationsOptions): ListPamConfigurationsResult { + return this.pamManager.listPamConfigurations(options ?? {}) + } + + public async createPamConfiguration(input: CreatePamConfigurationInput & { returnValue: true }): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput & { returnValue?: false } + ): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput + ): Promise + public async createPamConfiguration( + input: CreatePamConfigurationInput + ): Promise { + return this.pamManager.createPamConfiguration(input) + } + + public formatCreatePamConfigurationOutput(result: CreatePamConfigurationResult): string { + return this.pamManager.formatCreatePamConfigurationOutput(result) + } + + public async editPamConfiguration(input: EditPamConfigurationInput): Promise { + return this.pamManager.editPamConfiguration(input) + } + + public formatEditPamConfigurationOutput(result: EditPamConfigurationResult): string { + return this.pamManager.formatEditPamConfigurationOutput(result) + } + + public async removePamConfiguration(input: RemovePamConfigurationInput): Promise { + const result = await this.pamManager.removePamConfiguration(input) + if (result.success) await this.syncIfNeeded() + return result + } + + public formatRemovePamConfigurationOutput(result: RemovePamConfigurationResult): string { + return this.pamManager.formatRemovePamConfigurationOutput(result) + } + + public formatPamConfigurationsTable( + result: ListPamConfigurationsResult, + options?: FormatPamConfigurationsTableOptions + ): FormattedPamConfigurationsTable { + return this.pamManager.formatPamConfigurationsTable(result, options ?? {}) + } + + public renderPamConfigurationsAsciiTable( + table: FormattedPamConfigurationsTable, + options?: RenderPamConfigurationsAsciiTableOptions + ): string { + return this.pamManager.renderPamConfigurationsAsciiTable(table, options ?? {}) + } + + public formatPamConfigurationsJson( + result: ListPamConfigurationsResult, + options?: ListPamConfigurationsOptions + ): string { + return this.pamManager.formatPamConfigurationsJson(result, options ?? {}) + } + + public formatPamConfigurationsOutput( + result: ListPamConfigurationsResult, + options?: ListPamConfigurationsOptions + ): string { + return this.pamManager.formatPamConfigurationsOutput(result, options ?? {}) + } + public async shareFolder(input: ShareFolderInput): Promise { const result = await this.sharedFolderManager.shareFolder(input) if (result.success) await this.syncIfNeeded() diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index 740b72f..44948e7 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -60,6 +60,15 @@ "reports:audit-report": "ts-node src/enterpriseReport/audit_report.ts", "reports:action-report": "ts-node src/enterpriseReport/action_report.ts", "reports:password-report": "ts-node src/enterpriseReport/password_report.ts", + "pam:gateway:list": "ts-node src/pam/gateway/list_gateways.ts", + "pam:gateway:new": "ts-node src/pam/gateway/create_gateway.ts", + "pam:gateway:edit": "ts-node src/pam/gateway/edit_gateway.ts", + "pam:gateway:remove": "ts-node src/pam/gateway/remove_gateway.ts", + "pam:gateway:set-max-instances": "ts-node src/pam/gateway/set_max_instances.ts", + "pam:config:list": "ts-node src/pam/config/list_configs.ts", + "pam:config:new": "ts-node src/pam/config/create_config.ts", + "pam:config:edit": "ts-node src/pam/config/edit_config.ts", + "pam:config:remove": "ts-node src/pam/config/remove_config.ts", "link-local": "cd ../../KeeperSdk && npm link ../keeperapi && cd ../examples/sdk_example && npm link ../../keeperapi", "types": "tsc --watch", "types:ci": "tsc" diff --git a/examples/sdk_example/src/pam/config/configFieldPrompts.ts b/examples/sdk_example/src/pam/config/configFieldPrompts.ts new file mode 100644 index 0000000..dafcdb4 --- /dev/null +++ b/examples/sdk_example/src/pam/config/configFieldPrompts.ts @@ -0,0 +1,306 @@ +import { + logger, + prompt, + PAM_CONFIG_ENVIRONMENTS, + type PamConfigEnvironment, + type PamConfigurationPermissionValue, + type PamConfigurationPermissionsInput, + type PamConfigurationRecordFieldInput, +} from '@keeper-security/keeper-sdk-javascript' +import { isYes } from '../../utils/format' + +export { PAM_CONFIG_ENVIRONMENTS } +export type { PamConfigEnvironment } + +export type PamConfigFieldsPromptResult = { + fields: PamConfigurationRecordFieldInput[] + adminCredentialUid?: string +} + +export type PamConfigFieldsPromptOptions = { + includeSchedulePrompt?: boolean +} + +type PamConfigLabeledFieldType = 'text' | 'secret' | 'multiline' | 'json' | 'email' | 'checkbox' + +type PermissionPromptDefinition = { + key: keyof PamConfigurationPermissionsInput + label: string +} + +const PERMISSION_PROMPT_DEFINITIONS: readonly PermissionPromptDefinition[] = [ + { key: 'connections', label: 'Connections (-c)' }, + { key: 'tunneling', label: 'Tunneling (-u)' }, + { key: 'rotation', label: 'Rotation (-r)' }, + { key: 'remoteBrowserIsolation', label: 'Remote browser isolation (-rbi)' }, + { key: 'connectionsRecording', label: 'Connections recording (-cr)' }, + { key: 'typescriptRecording', label: 'Typescript recording (-tr)' }, + { key: 'aiThreatDetection', label: 'AI threat detection' }, + { key: 'aiTerminateSessionOnDetection', label: 'AI terminate session on detection' }, +] + +async function promptOptionalText(label: string): Promise { + return (await prompt(`${label} (optional): `)).trim() +} + +async function promptOptionalBoolean(label: string): Promise { + const raw = (await prompt(`${label} [y/N, Enter to skip]: `)).trim() + if (!raw) return undefined + return isYes(raw) +} + +function splitCommaSeparatedList(raw: string): string[] { + return raw + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) +} + +function appendLabeledField( + fields: PamConfigurationRecordFieldInput[], + type: PamConfigLabeledFieldType, + label: string, + value: string | boolean | undefined +): void { + if (value == null) return + if (typeof value === 'string' && value.length === 0) return + fields.push({ type, label, value: [value] }) +} + +function appendMultilineField( + fields: PamConfigurationRecordFieldInput[], + label: string, + values: string[] +): void { + if (values.length === 0) return + fields.push({ type: 'multiline', label, value: [values.join('\n')] }) +} + +async function promptPermissionFlagValue(label: string): Promise { + const raw = (await prompt(`${label} [on|off|default, Enter to skip]: `)).trim().toLowerCase() + if (!raw) return undefined + if (raw === 'on' || raw === 'off' || raw === 'default') return raw + logger.info(` Invalid "${raw}". Skipping (use on, off, or default).`) + return undefined +} + +export async function promptPamConfigurationPermissions(): Promise< + PamConfigurationPermissionsInput | undefined +> { + const wantPermissions = isYes(await prompt('Set additional permissions? [y/N]: ')) + if (!wantPermissions) return undefined + + const permissions: PamConfigurationPermissionsInput = {} + let anySet = false + for (const entry of PERMISSION_PROMPT_DEFINITIONS) { + const value = await promptPermissionFlagValue(entry.label) + if (value) { + permissions[entry.key] = value + anySet = true + } + } + return anySet ? permissions : undefined +} + +async function promptAwsConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'awsId', await promptOptionalText('AWS ID (--aws-id)')) + appendLabeledField(fields, 'secret', 'accessKeyId', await promptOptionalText('Access Key ID (--access-key-id)')) + appendLabeledField( + fields, + 'secret', + 'accessSecretKey', + await promptOptionalText('Access Secret Key (--access-secret-key)') + ) + appendMultilineField( + fields, + 'regionNames', + splitCommaSeparatedList(await promptOptionalText('Region names, comma-separated (--region-name)')) + ) +} + +async function promptAzureConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'azureId', await promptOptionalText('Azure ID (--azure-id)')) + appendLabeledField(fields, 'secret', 'clientId', await promptOptionalText('Client ID (--client-id)')) + appendLabeledField(fields, 'secret', 'clientSecret', await promptOptionalText('Client Secret (--client-secret)')) + appendLabeledField( + fields, + 'secret', + 'subscriptionId', + await promptOptionalText('Subscription ID (--subscription_id)') + ) + appendLabeledField(fields, 'secret', 'tenantId', await promptOptionalText('Tenant ID (--tenant-id)')) + appendMultilineField( + fields, + 'resourceGroups', + splitCommaSeparatedList(await promptOptionalText('Resource groups, comma-separated (--resource-group)')) + ) +} + +async function promptGcpConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'pamGcpId', await promptOptionalText('GCP ID (--gcp-id)')) + appendLabeledField( + fields, + 'json', + 'pamServiceAccountKey', + await promptOptionalText('Service Account Key JSON (--service-account-key)') + ) + appendLabeledField( + fields, + 'email', + 'pamGoogleAdminEmail', + await promptOptionalText('Google Admin Email (--google-admin-email)') + ) + appendMultilineField( + fields, + 'pamGcpRegionName', + splitCommaSeparatedList(await promptOptionalText('GCP regions, comma-separated (--gcp-region)')) + ) +} + +async function promptGitHubConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'pamGitHubId', await promptOptionalText('GitHub ID (--github-id)')) + appendLabeledField( + fields, + 'secret', + 'personalAccessToken', + await promptOptionalText('Personal Access Token (--personal-access-token)') + ) + appendLabeledField( + fields, + 'text', + 'pamGitHubBaseUrl', + await promptOptionalText('GitHub Base URL (--github-base-url)') + ) +} + +async function promptDomainConfigurationFields( + fields: PamConfigurationRecordFieldInput[] +): Promise { + appendLabeledField(fields, 'text', 'pamDomainId', await promptOptionalText('Domain ID (--domain-id)')) + const hostname = await promptOptionalText('Domain hostname (--domain-hostname)') + const port = await promptOptionalText('Domain port (--domain-port)') + if (hostname || port) { + fields.push({ + type: 'pamHostname', + value: [{ hostName: hostname || '', port: port || '' }], + }) + } + const useSsl = await promptOptionalBoolean('Use SSL (--domain-use-ssl)') + if (useSsl != null) appendLabeledField(fields, 'checkbox', 'useSSL', useSsl) + const scanDcCidr = await promptOptionalBoolean('Scan DC CIDR (--domain-scan-dc-cidr)') + if (scanDcCidr != null) appendLabeledField(fields, 'checkbox', 'scanDCCIDR', scanDcCidr) + appendLabeledField( + fields, + 'text', + 'networkCIDR', + await promptOptionalText('Domain network CIDR (--domain-network-cidr)') + ) + appendLabeledField( + fields, + 'text', + 'userMatch', + await promptOptionalText('Domain user match (--domain-user-match)') + ) + const domainAdmin = await promptOptionalText('Domain admin pamUser UID/title (--domain-admin)') + return domainAdmin || undefined +} + +async function promptOciConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'pamOciId', await promptOptionalText('OCI ID (--oci-id)')) + appendLabeledField(fields, 'secret', 'adminOcid', await promptOptionalText('OCI Admin OCID (--oci-admin-id)')) + appendLabeledField( + fields, + 'secret', + 'adminPublicKey', + await promptOptionalText('OCI Admin Public Key (--oci-admin-public-key)') + ) + appendLabeledField( + fields, + 'secret', + 'adminPrivateKey', + await promptOptionalText('OCI Admin Private Key (--oci-admin-private-key)') + ) + appendLabeledField(fields, 'text', 'tenancyOci', await promptOptionalText('OCI Tenancy (--oci-tenancy)')) + appendLabeledField(fields, 'text', 'regionOci', await promptOptionalText('OCI Region (--oci-region)')) +} + +async function promptLocalConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'networkId', await promptOptionalText('Network ID (--network-id)')) + appendLabeledField(fields, 'text', 'networkCIDR', await promptOptionalText('Network CIDR (--network-cidr)')) +} + +async function promptCommonOptionalFields( + fields: PamConfigurationRecordFieldInput[], + options: PamConfigFieldsPromptOptions +): Promise { + appendLabeledField( + fields, + 'text', + 'identityProviderUid', + await promptOptionalText('Identity Provider UID (--identity-provider)') + ) + + if (options.includeSchedulePrompt !== false) { + const scheduleCron = (await prompt('Default rotation CRON (--schedule, Enter to skip): ')).trim() + if (scheduleCron) { + fields.push({ + type: 'schedule', + label: 'defaultRotationSchedule', + value: [{ type: 'CRON', cron: scheduleCron, tz: 'Etc/UTC' }], + }) + } + } + + const portMappingsRaw = await promptOptionalText( + 'Port mappings, comma-separated port=protocol (--port-mapping)' + ) + if (!portMappingsRaw) return + + const portMappingLines = portMappingsRaw + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => { + if (entry.includes('=')) return entry + const [port, protocol] = entry.split(':') + return protocol ? `${port.trim()}=${protocol.trim()}` : port.trim() + }) + .filter(Boolean) + appendMultilineField(fields, 'portMapping', portMappingLines) +} + +export async function promptPamConfigurationFields( + environment: string, + options: PamConfigFieldsPromptOptions = {} +): Promise { + const fields: PamConfigurationRecordFieldInput[] = [] + let adminCredentialUid: string | undefined + + switch (environment) { + case 'aws': + await promptAwsConfigurationFields(fields) + break + case 'azure': + await promptAzureConfigurationFields(fields) + break + case 'gcp': + await promptGcpConfigurationFields(fields) + break + case 'github': + await promptGitHubConfigurationFields(fields) + break + case 'domain': + adminCredentialUid = await promptDomainConfigurationFields(fields) + break + case 'oci': + await promptOciConfigurationFields(fields) + break + case 'local': + default: + await promptLocalConfigurationFields(fields) + break + } + + await promptCommonOptionalFields(fields, options) + return { fields, adminCredentialUid } +} diff --git a/examples/sdk_example/src/pam/config/create_config.ts b/examples/sdk_example/src/pam/config/create_config.ts new file mode 100644 index 0000000..8b8239d --- /dev/null +++ b/examples/sdk_example/src/pam/config/create_config.ts @@ -0,0 +1,84 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + resolvePamConfigurationRecordType, + suppressLogs, + type CreatePamConfigurationResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' +import { + PAM_CONFIG_ENVIRONMENTS, + promptPamConfigurationFields, + promptPamConfigurationPermissions, +} from './configFieldPrompts' + +async function createPamConfigurationExample() { + const vault = await login() + + try { + const environment = (await prompt(`Environment (${PAM_CONFIG_ENVIRONMENTS.join('|')}): `)).trim().toLowerCase() + if (!environment) { + logger.info('Environment is required.') + return + } + const configType = resolvePamConfigurationRecordType(environment) + if (!configType) { + logger.info(`Invalid environment. Choose one of: ${PAM_CONFIG_ENVIRONMENTS.join(', ')}`) + return + } + + const title = (await prompt('Configuration title: ')).trim() + if (!title) { + logger.info('Title is required.') + return + } + + const sharedFolder = (await prompt('Shared folder or Nested Share Folder UID or name: ')).trim() + if (!sharedFolder) { + logger.info('Shared folder or Nested Share Folder is required.') + return + } + + const gateway = (await prompt('Gateway UID or name (optional): ')).trim() || undefined + const { fields, adminCredentialUid } = await promptPamConfigurationFields(environment) + const permissions = await promptPamConfigurationPermissions() + const returnValue = isYes(await prompt('Return value only (automation / -r)? [y/N]: ')) + + let result: CreatePamConfigurationResult | string + const restore = suppressLogs() + try { + result = await vault.createPamConfiguration({ + title, + configType, + sharedFolder, + gateway, + fields, + adminCredentialUid, + permissions, + returnValue, + }) + } finally { + restore() + } + + if (returnValue) { + logger.info(result as string) + return + } + + logger.info('') + logger.info(vault.formatCreatePamConfigurationOutput(result as CreatePamConfigurationResult)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(createPamConfigurationExample) diff --git a/examples/sdk_example/src/pam/config/edit_config.ts b/examples/sdk_example/src/pam/config/edit_config.ts new file mode 100644 index 0000000..d2abbc9 --- /dev/null +++ b/examples/sdk_example/src/pam/config/edit_config.ts @@ -0,0 +1,117 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + resolvePamConfigurationRecordType, + suppressLogs, + type EditPamConfigurationInput, + type EditPamConfigurationResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' +import { + PAM_CONFIG_ENVIRONMENTS, + promptPamConfigurationFields, + promptPamConfigurationPermissions, +} from './configFieldPrompts' + +async function editPamConfigurationExample() { + const vault = await login() + + try { + const configurationUidOrTitle = (await prompt('PAM Configuration UID or title: ')).trim() + if (!configurationUidOrTitle) { + logger.info('Configuration UID or title is required.') + return + } + + const titleRaw = (await prompt('New title (Enter to keep): ')).trim() + const title = titleRaw || undefined + + const environmentRaw = ( + await prompt(`New environment (${PAM_CONFIG_ENVIRONMENTS.join('|')}, Enter to keep): `) + ) + .trim() + .toLowerCase() + let configType: string | undefined + let environmentForFields = '' + if (environmentRaw) { + configType = resolvePamConfigurationRecordType(environmentRaw) + if (!configType) { + logger.info(`Invalid environment. Choose one of: ${PAM_CONFIG_ENVIRONMENTS.join(', ')}`) + return + } + environmentForFields = environmentRaw + } + + const sharedFolderRaw = ( + await prompt('Shared folder or Nested Share Folder UID or name (Enter to keep): ') + ).trim() + const sharedFolder = sharedFolderRaw || undefined + + const gatewayPrompt = (await prompt('Gateway UID or name (Enter to keep, "-" to clear): ')).trim() + let gateway: string | undefined + if (gatewayPrompt === '-') gateway = '' + else if (gatewayPrompt) gateway = gatewayPrompt + + const updateFields = isYes(await prompt('Update environment / schedule / port-mapping fields? [y/N]: ')) + if (updateFields && !environmentForFields) { + environmentForFields = ( + await prompt(`Environment for field prompts (${PAM_CONFIG_ENVIRONMENTS.join('|')}): `) + ) + .trim() + .toLowerCase() + if (!resolvePamConfigurationRecordType(environmentForFields)) { + logger.info(`Invalid environment. Choose one of: ${PAM_CONFIG_ENVIRONMENTS.join(', ')}`) + return + } + } + + const fieldPrompt = updateFields ? await promptPamConfigurationFields(environmentForFields) : undefined + const fields = fieldPrompt?.fields + const adminCredentialUid = fieldPrompt?.adminCredentialUid + + const removeRaw = (await prompt('Remove resource record UIDs/titles (comma-separated, optional): ')).trim() + const removeResourceRecords = removeRaw + ? removeRaw + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + : undefined + + const permissions = await promptPamConfigurationPermissions() + + const input: EditPamConfigurationInput = { + configurationUidOrTitle, + title, + configType, + sharedFolder, + gateway, + fields, + adminCredentialUid, + removeResourceRecords, + permissions, + } + + let result: EditPamConfigurationResult + const restore = suppressLogs() + try { + result = await vault.editPamConfiguration(input) + } finally { + restore() + } + + logger.info('') + logger.info(vault.formatEditPamConfigurationOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(editPamConfigurationExample) diff --git a/examples/sdk_example/src/pam/config/list_configs.ts b/examples/sdk_example/src/pam/config/list_configs.ts new file mode 100644 index 0000000..3d3829b --- /dev/null +++ b/examples/sdk_example/src/pam/config/list_configs.ts @@ -0,0 +1,54 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + PamConfigListFormat, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function listPamConfigurationsExample() { + const vault = await login() + + try { + const configUid = (await prompt('PAM Configuration UID (Enter for all): ')).trim() || undefined + const verbose = isYes(await prompt('Verbose output? [y/N]: ')) + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + + const options = { + configUid, + verbose, + format: asJson ? PamConfigListFormat.Json : PamConfigListFormat.Table, + } + + let result + const restore = suppressLogs() + try { + result = vault.listPamConfigurations(options) + } finally { + restore() + } + + if (!result.detail && result.configurations.length === 0) { + for (const warning of result.warnings) { + logger.warn(warning) + } + logger.info(result.message || 'No PAM configurations found.') + return + } + + logger.info('') + logger.info(vault.formatPamConfigurationsOutput(result, options)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listPamConfigurationsExample) diff --git a/examples/sdk_example/src/pam/config/remove_config.ts b/examples/sdk_example/src/pam/config/remove_config.ts new file mode 100644 index 0000000..679fbe5 --- /dev/null +++ b/examples/sdk_example/src/pam/config/remove_config.ts @@ -0,0 +1,44 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function removePamConfigurationExample() { + const vault = await login() + + try { + const configurationUidOrTitle = (await prompt('PAM Configuration UID or title: ')).trim() + if (!configurationUidOrTitle) { + logger.info('Configuration UID or title is required.') + return + } + + let result + const restore = suppressLogs() + try { + result = await vault.removePamConfiguration({ configurationUidOrTitle }) + } finally { + restore() + } + + logger.info('') + if (!result.found) { + logger.warn(vault.formatRemovePamConfigurationOutput(result)) + } else { + logger.info(vault.formatRemovePamConfigurationOutput(result)) + } + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(removePamConfigurationExample) diff --git a/examples/sdk_example/src/pam/gateway/create_gateway.ts b/examples/sdk_example/src/pam/gateway/create_gateway.ts new file mode 100644 index 0000000..5109c31 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/create_gateway.ts @@ -0,0 +1,75 @@ +import { + cleanup, + extractErrorMessage, + GatewayConfigInitFormat, + login, + logger, + prompt, + suppressLogs, + type CreateGatewayResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function createGatewayExample() { + const vault = await login() + + try { + const name = (await prompt('Gateway name: ')).trim() + if (!name) { + logger.info('Gateway name is required.') + return + } + + const application = (await prompt('KSM application name or UID: ')).trim() + if (!application) { + logger.info('KSM application is required.') + return + } + + const expireRaw = (await prompt('Token expires in minutes [60]: ')).trim() + const tokenExpiresInMin = expireRaw ? Number(expireRaw) : 60 + + const wantConfig = isYes(await prompt('Initialize config (json/b64) instead of raw OTT? [y/N]: ')) + let configInit: GatewayConfigInitFormat | undefined + if (wantConfig) { + const format = (await prompt('Config format — json or b64 [json]: ')).trim().toLowerCase() || 'json' + configInit = + format === GatewayConfigInitFormat.B64 || format === 'b64' + ? GatewayConfigInitFormat.B64 + : GatewayConfigInitFormat.Json + } + + // Automation / Commander -r: print only tokenOrConfig (no banner). + const returnValueOnly = isYes(await prompt('Return value only (automation / -r)? [y/N]: ')) + + let result: CreateGatewayResult + const restore = suppressLogs() + try { + result = await vault.createGateway({ + name, + application, + tokenExpiresInMin, + configInit, + }) + } finally { + restore() + } + + if (returnValueOnly) { + logger.info(result.tokenOrConfig) + return + } + + logger.info('') + logger.info(vault.formatCreateGatewayOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(createGatewayExample) diff --git a/examples/sdk_example/src/pam/gateway/edit_gateway.ts b/examples/sdk_example/src/pam/gateway/edit_gateway.ts new file mode 100644 index 0000000..53e46e7 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/edit_gateway.ts @@ -0,0 +1,49 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function editGatewayExample() { + const vault = await login() + + try { + const gatewayUidOrName = (await prompt('Gateway UID or name: ')).trim() + if (!gatewayUidOrName) { + logger.info('Gateway UID or name is required.') + return + } + + const name = (await prompt('New name (Enter to keep current): ')).trim() || undefined + const nodeIdRaw = (await prompt('New node ID or name (Enter to keep current): ')).trim() + const nodeIdOrName = nodeIdRaw || undefined + + if (!name && !nodeIdOrName) { + logger.info('Nothing to do. Provide at least a new name or node.') + return + } + + let result + const restore = suppressLogs() + try { + result = await vault.editGateway({ gatewayUidOrName, name, nodeIdOrName }) + } finally { + restore() + } + + logger.info('') + logger.info(vault.formatEditGatewayOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(editGatewayExample) diff --git a/examples/sdk_example/src/pam/gateway/list_gateways.ts b/examples/sdk_example/src/pam/gateway/list_gateways.ts new file mode 100644 index 0000000..42d75af --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/list_gateways.ts @@ -0,0 +1,58 @@ +import { + cleanup, + extractErrorMessage, + GatewayListFormat, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function listGatewaysExample() { + const vault = await login() + + try { + const force = isYes(await prompt('Force list if router is down? [y/N]: ')) + const verbose = isYes(await prompt('Verbose output? [y/N]: ')) + const onlineOnly = isYes(await prompt('Online gateways only? [y/N]: ')) + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + + const options = { + force, + verbose, + onlineOnly, + format: asJson ? GatewayListFormat.Json : GatewayListFormat.Table, + } + + let result + const restore = suppressLogs() + try { + result = await vault.listGateways(options) + } finally { + restore() + } + + if (result.aborted) { + logger.info(result.message || 'Router unavailable. Re-run with force to list gateways.') + return + } + + if (result.gateways.length === 0) { + logger.info(result.message || 'No gateways found.') + return + } + + logger.info('') + logger.info(vault.formatGatewaysOutput(result, options)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listGatewaysExample) diff --git a/examples/sdk_example/src/pam/gateway/remove_gateway.ts b/examples/sdk_example/src/pam/gateway/remove_gateway.ts new file mode 100644 index 0000000..b0a01ee --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/remove_gateway.ts @@ -0,0 +1,40 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function removeGatewayExample() { + const vault = await login() + + try { + const gatewayUidOrName = (await prompt('Gateway UID or name: ')).trim() + if (!gatewayUidOrName) { + logger.info('Gateway UID or name is required.') + return + } + + let result + const restore = suppressLogs() + try { + result = await vault.removeGateway({ gatewayUidOrName }) + } finally { + restore() + } + + logger.info('') + logger.info(vault.formatRemoveGatewayOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(removeGatewayExample) diff --git a/examples/sdk_example/src/pam/gateway/set_max_instances.ts b/examples/sdk_example/src/pam/gateway/set_max_instances.ts new file mode 100644 index 0000000..86d0710 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/set_max_instances.ts @@ -0,0 +1,56 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + MAX_GATEWAY_MAX_INSTANCES, + MIN_GATEWAY_MAX_INSTANCES, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function setGatewayMaxInstancesExample() { + const vault = await login() + + try { + const gatewayUidOrName = (await prompt('Gateway UID or name: ')).trim() + if (!gatewayUidOrName) { + logger.info('Gateway UID or name is required.') + return + } + + const maxRaw = ( + await prompt(`Maximum instances (${MIN_GATEWAY_MAX_INSTANCES}-${MAX_GATEWAY_MAX_INSTANCES}): `) + ).trim() + if (!/^\d+$/.test(maxRaw)) { + logger.info( + `Maximum instances must be an integer between ${MIN_GATEWAY_MAX_INSTANCES} and ${MAX_GATEWAY_MAX_INSTANCES}.` + ) + return + } + const maxInstances = Number.parseInt(maxRaw, 10) + + let result + const restore = suppressLogs() + try { + result = await vault.setGatewayMaxInstances({ + gatewayUidOrName, + maxInstances, + }) + } finally { + restore() + } + + logger.info('') + logger.info(vault.formatSetGatewayMaxInstancesOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(setGatewayMaxInstancesExample) diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index e830174..c6abece 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -12,6 +12,7 @@ import { GraphSync, PAM, Records, + Router, ServiceLogger, SsoCloud, Vault, @@ -961,11 +962,34 @@ export const automatorAdminResetMessage = ( export const getControllers = (): RestOutMessage => createOutMessage('pam/get_controllers', PAM.PAMControllersResponse) +export const modifyControllerMessage = (data: PAM.IPAMController): RestInMessage => + createInMessage(data, 'pam/modify_controller', PAM.PAMController) + +export const removeControllerMessage = ( + data: PAM.IPAMGenericUidRequest +): RestMessage => + createMessage(data, 'pam/remove_controller', PAM.PAMGenericUidRequest, PAM.PAMRemoveControllerResponse) + +export const setControllerMaxInstanceCountMessage = ( + data: PAM.IPAMSetMaxInstanceCountRequest +): RestInMessage => + createInMessage(data, 'pam/set_controller_max_instance_count', PAM.PAMSetMaxInstanceCountRequest) + export const getConfigurationControllerMessage = ( data: PAM.IPAMGenericUidRequest ): RestMessage => createMessage(data, 'pam/get_configuration_controller', PAM.PAMGenericUidRequest, PAM.PAMController) +export const addConfigurationRecordMessage = ( + data: PAM.IConfigurationAddRequest +): RestInMessage => + createInMessage(data, 'pam/add_configuration_record', PAM.ConfigurationAddRequest) + +export const setConfigurationControllerMessage = ( + data: PAM.IPAMConfigurationController +): RestInMessage => + createInMessage(data, 'pam/set_configuration_controller', PAM.PAMConfigurationController) + /* -- PAM Router (DAG GraphSync) -- */ export const pamSyncMessage = ( @@ -996,6 +1020,12 @@ export const pamGetLeafsMessage = ( export const pamGetOnlineControllersMessage = (): RestOutMessage => createOutMessage('api/user/get_controllers', PAM.PAMOnlineControllers) +/** Layer-B: set PAM configuration network-level `allowedSettings` (permissions). */ +export const pamConfigureNetworkGraphMessage = ( + data: Router.IPAMNetworkConfigurationRequest +): RestInMessage => + createInMessage(data, 'api/user/configure_network_graph', Router.PAMNetworkConfigurationRequest) + export const readWorkflowConfigMessage = ( data: GraphSync.IGraphSyncRef ): RestMessage => @@ -1060,6 +1090,17 @@ export const keeperDriveRecordsAdd = ( ): RestMessage => createMessage(data, 'vault/records/v3/add', record.v3.RecordsAddRequest, Records.RecordsModifyResponse) +/** NSF: create a PAM configuration record already linked into a nested share folder. */ +export const addPamConfigurationV3Message = ( + data: record.v3.IRecordsAddRequest +): RestMessage => + createMessage( + data, + 'vault/records/v3/add_pam_configuration', + record.v3.RecordsAddRequest, + Records.RecordsModifyResponse + ) + export const keeperDriveRecordsUpdate = ( data: Records.IRecordsUpdateRequest ): RestMessage =>