From 7679f64bc057423faa731df3ffe19834906363b6 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 30 Apr 2026 18:18:59 +0530 Subject: [PATCH 01/23] Folder and shared folder implementation (#131) --- KeeperSdk/src/records/Totp.ts | 5 +---- KeeperSdk/src/utils/index.ts | 2 ++ KeeperSdk/src/utils/patterns.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/KeeperSdk/src/records/Totp.ts b/KeeperSdk/src/records/Totp.ts index 1ba67d53..f0b0f31e 100644 --- a/KeeperSdk/src/records/Totp.ts +++ b/KeeperSdk/src/records/Totp.ts @@ -22,10 +22,7 @@ const DEFAULT_ALGORITHM: TotpAlgorithm = 'SHA1' const UINT32_MAX = 0x100000000 function decodeBase32(input: string): Uint8Array { - const noWhitespace = input.replace(/\s+/g, '') - let endIndex = noWhitespace.length - while (endIndex > 0 && noWhitespace.charCodeAt(endIndex - 1) === 0x3d) endIndex-- - const cleaned = noWhitespace.slice(0, endIndex).toUpperCase() + const cleaned = input.replace(/=+$/g, '').replace(/\s+/g, '').toUpperCase() const out: number[] = [] let buffer = 0 let bits = 0 diff --git a/KeeperSdk/src/utils/index.ts b/KeeperSdk/src/utils/index.ts index a7a638f0..7b53cf56 100644 --- a/KeeperSdk/src/utils/index.ts +++ b/KeeperSdk/src/utils/index.ts @@ -20,6 +20,8 @@ export { EMAIL_LIST_SEPARATOR_PATTERN, TOKEN_SEPARATOR_PATTERN, REGEX_ESCAPE_PATTERN, + TRAILING_EQUALS_PATTERN, + WHITESPACE_PATTERN, isValidEmail, escapeRegExp, resolveSearchPattern, diff --git a/KeeperSdk/src/utils/patterns.ts b/KeeperSdk/src/utils/patterns.ts index d65cc00f..fe15b54c 100644 --- a/KeeperSdk/src/utils/patterns.ts +++ b/KeeperSdk/src/utils/patterns.ts @@ -12,10 +12,14 @@ export const TOKEN_SEPARATOR_PATTERN = /[\s\-_.,;:!?@#$%^&*()[\]{}|\\/<>]+/ /** Characters that must be escaped when embedding user input into a RegExp. */ export const REGEX_ESCAPE_PATTERN = /[.+^${}()|[\]\\]/g -const MAX_EMAIL_LENGTH = 254 +/** Sequence of one or more `=` characters at end of string (Base32 padding). */ +export const TRAILING_EQUALS_PATTERN = /=+$/g + +/** Any whitespace run. */ +export const WHITESPACE_PATTERN = /\s+/g export function isValidEmail(value: string): boolean { - return value.length <= MAX_EMAIL_LENGTH && EMAIL_PATTERN.test(value) + return EMAIL_PATTERN.test(value) } export function escapeRegExp(value: string): string { From 84ec8ee0adc0558a162681e1182b990d3cee2d84 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 11 May 2026 09:52:48 +0530 Subject: [PATCH 02/23] added a basic working copy of client-server arch initial commit for combined cli. refactor pending added a small icon updated gitignore --- KeeperSdk/package-lock.json | 543 +------ KeeperSdk/package.json | 3 +- KeeperSdk/src/vault/KeeperVault.ts | 23 + KeeperSdk/tsconfig.json | 2 +- examples/sdk_example/package-lock.json | 410 +---- examples/sdk_example/package.json | 8 +- keeperapi/package-lock.json | 155 +- keeperapi/package.json | 2 +- keeperapi/scripts/copyProtoDts.js | 15 + shellcomponent/.gitignore | 5 + shellcomponent/README.md | 116 ++ shellcomponent/index.html | 32 + shellcomponent/keeper-shell.d.ts | 56 + shellcomponent/package-lock.json | 1379 +++++++++++++++++ shellcomponent/package.json | 49 + shellcomponent/src/KeeperShell.ts | 1141 ++++++++++++++ shellcomponent/src/bufferPolyfill.ts | 6 + shellcomponent/src/cli/cliCommandDocs.ts | 224 +++ shellcomponent/src/cli/cliComplete.ts | 99 ++ shellcomponent/src/cli/cliContext.ts | 28 + shellcomponent/src/cli/cliDispatch.ts | 51 + shellcomponent/src/cli/cliHelp.ts | 99 ++ shellcomponent/src/cli/cliParse.ts | 174 +++ .../src/cli/inMemoryConfigLoader.ts | 43 + shellcomponent/src/cli/keeperCommands.ts | 368 +++++ shellcomponent/src/cli/mkdir.ts | 25 + shellcomponent/src/cli/types.ts | 12 + shellcomponent/src/dev-bootstrap.ts | 42 + shellcomponent/src/devApiLogger.ts | 137 ++ shellcomponent/src/index.ts | 17 + shellcomponent/src/shims/fs-empty.ts | 13 + shellcomponent/src/shims/fs-promises-empty.ts | 21 + shellcomponent/src/shims/os-homedir.ts | 6 + shellcomponent/src/vite-env.d.ts | 10 + shellcomponent/tsconfig.json | 19 + shellcomponent/vite.config.ts | 63 + 36 files changed, 4474 insertions(+), 922 deletions(-) create mode 100644 keeperapi/scripts/copyProtoDts.js create mode 100644 shellcomponent/.gitignore create mode 100644 shellcomponent/README.md create mode 100644 shellcomponent/index.html create mode 100644 shellcomponent/keeper-shell.d.ts create mode 100644 shellcomponent/package-lock.json create mode 100644 shellcomponent/package.json create mode 100644 shellcomponent/src/KeeperShell.ts create mode 100644 shellcomponent/src/bufferPolyfill.ts create mode 100644 shellcomponent/src/cli/cliCommandDocs.ts create mode 100644 shellcomponent/src/cli/cliComplete.ts create mode 100644 shellcomponent/src/cli/cliContext.ts create mode 100644 shellcomponent/src/cli/cliDispatch.ts create mode 100644 shellcomponent/src/cli/cliHelp.ts create mode 100644 shellcomponent/src/cli/cliParse.ts create mode 100644 shellcomponent/src/cli/inMemoryConfigLoader.ts create mode 100644 shellcomponent/src/cli/keeperCommands.ts create mode 100644 shellcomponent/src/cli/mkdir.ts create mode 100644 shellcomponent/src/cli/types.ts create mode 100644 shellcomponent/src/dev-bootstrap.ts create mode 100644 shellcomponent/src/devApiLogger.ts create mode 100644 shellcomponent/src/index.ts create mode 100644 shellcomponent/src/shims/fs-empty.ts create mode 100644 shellcomponent/src/shims/fs-promises-empty.ts create mode 100644 shellcomponent/src/shims/os-homedir.ts create mode 100644 shellcomponent/src/vite-env.d.ts create mode 100644 shellcomponent/tsconfig.json create mode 100644 shellcomponent/vite.config.ts diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 0eed8f14..0d5ab53d 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -66,417 +66,38 @@ "faye-websocket": "^0.11.3", "form-data": "^4.0.4", "node-rsa": "^1.0.8" - } - }, - "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/" + "devDependencies": { + "@babel/preset-env": "^7.23.5", + "@babel/preset-typescript": "^7.23.3", + "@rollup/plugin-commonjs": "^22.0.1", + "@rollup/plugin-node-resolve": "^13.3.0", + "@types/jest": "^24.0.15", + "@types/node": "^20.9.1", + "jest": "^29.6.1", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.8.1", + "protobufjs": "^7.2.4", + "protobufjs-cli": "^1.1.1", + "rollup": "^2.79.2", + "rollup-plugin-typescript2": "^0.32.1", + "ts-jest": "^29.1.1", + "ts-node": "^8.10.2", + "typescript": "^4.0.1" } }, - "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/@keeper-security/keeperapi": { + "resolved": "../keeperapi", + "link": true }, "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.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "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.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "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==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "dev": true, "license": "MIT", "dependencies": { - "asn1": "^0.2.4" + "undici-types": "~7.21.0" } }, "node_modules/prettier": { @@ -495,75 +116,6 @@ "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 - } - } - }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -578,48 +130,11 @@ } }, "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==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "dev": true, "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 7a1fe816..48fc92e6 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -8,7 +8,8 @@ "directory": "KeeperSdk" }, "license": "ISC", - "main": "src/index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "tsc", "clean": "rm -rf dist", diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 52c3055e..7ed7726f 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -8,6 +8,7 @@ import { DTeam, DUserFolder, Authentication, + normal64Bytes, } from '@keeper-security/keeperapi' import type { SyncResult, SyncLogFormat, VaultStorage, SessionStorage, AuthUI3 } from '@keeper-security/keeperapi' import { InMemoryStorage } from '../storage/InMemoryStorage' @@ -265,6 +266,28 @@ export class KeeperVault { this.log.info(`Logged in as ${username} (via session token)`) } + /** + * Persist device token and private key for this vault host in session storage so + * {@link loginWithSessionToken} and {@link resumeSession} work without a prior password login on this machine. + * Values use the same base64 / base64url decoding as {@link SessionManager} (`normal64Bytes`). + */ + public async registerDevice( + deviceToken: string, + privateKey: string, + options?: { username?: string } + ): Promise { + const host = this.config.host + const save = this.sessionManager.createOnDeviceConfig(host) + await save({ + deviceToken: normal64Bytes(deviceToken), + privateKey: normal64Bytes(privateKey), + }) + if (options?.username) { + this.sessionManager.setLastUsername(options.username) + } + this.log.info(`Device credentials stored for host ${host}`) + } + public getSessionToken(): string | undefined { return this.auth?.sessionToken || undefined } diff --git a/KeeperSdk/tsconfig.json b/KeeperSdk/tsconfig.json index 74c99318..15eafc2b 100644 --- a/KeeperSdk/tsconfig.json +++ b/KeeperSdk/tsconfig.json @@ -7,7 +7,7 @@ "skipLibCheck": true, "esModuleInterop": true, "declaration": true, - "rootDir": ".", + "rootDir": "src", "outDir": "dist", "types": ["node"] }, diff --git a/examples/sdk_example/package-lock.json b/examples/sdk_example/package-lock.json index 052d934b..22c3a460 100644 --- a/examples/sdk_example/package-lock.json +++ b/examples/sdk_example/package-lock.json @@ -8,11 +8,12 @@ "name": "sdk-example", "version": "1.0.0", "dependencies": { - "@keeper-security/keeperapi": "17.1.0", - "@types/node": "^20.9.1", "ts-node": "^10.7.0", "tsconfig-paths": "^4.2.0", "typescript": "^4.6.3" + }, + "devDependencies": { + "@types/node": "^25.6.2" } }, "node_modules/@cspotcode/source-map-support": { @@ -52,62 +53,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@keeper-security/keeperapi": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.1.0.tgz", - "integrity": "sha512-BV1o72g1BXz3Agjw0l6kLlS5SOvWCJ0qD2RcEXPAvMQJcq0O8SLaf2KhIG/dGHPHdNZ5Dod4+cI0sAfnpZeYZw==", - "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" - } - }, - "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", @@ -133,12 +78,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "license": "MIT", + "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.21.0" } }, "node_modules/acorn": { @@ -171,67 +117,12 @@ "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", @@ -241,196 +132,6 @@ "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.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "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.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "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/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -449,36 +150,6 @@ "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/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -488,41 +159,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/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/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -594,6 +230,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -603,9 +240,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "license": "MIT" }, "node_modules/v8-compile-cache-lib": { @@ -614,29 +251,6 @@ "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", diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index f53fc689..f5457fcc 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -44,13 +44,15 @@ "users:user-team": "ts-node src/users/team_user.ts", "link-local": "cd ../../KeeperSdk && npm link ../keeperapi && cd ../examples/sdk_example && npm link ../../keeperapi", "types": "tsc --watch", - "types:ci": "tsc" + "types:ci": "tsc", + "build": "tsc" }, "dependencies": { - "@keeper-security/keeperapi": "17.1.0", - "@types/node": "^20.9.1", "ts-node": "^10.7.0", "tsconfig-paths": "^4.2.0", "typescript": "^4.6.3" + }, + "devDependencies": { + "@types/node": "^25.6.2" } } diff --git a/keeperapi/package-lock.json b/keeperapi/package-lock.json index bb3ca614..387c5be5 100644 --- a/keeperapi/package-lock.json +++ b/keeperapi/package-lock.json @@ -76,6 +76,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -1786,6 +1787,32 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2794,6 +2821,38 @@ "node": ">= 10" } }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -2905,6 +2964,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, + "peer": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -2921,6 +2981,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.2.tgz", "integrity": "sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2979,6 +3040,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3362,6 +3424,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -3608,6 +3671,14 @@ "url": "https://opencollective.com/core-js" } }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4647,6 +4718,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.1.tgz", "integrity": "sha512-Nirw5B4nn69rVUZtemCQhwxOBhm0nsp3hmtF4rzCeWD7BkjAXRIji7xWQfnTNbz9g0aVsBX6aZK3n+23LM6uDw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.6.1", "@jest/types": "^29.6.1", @@ -7292,7 +7364,7 @@ "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", "dev": true, "hasInstallScript": true, - "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7669,7 +7741,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, - "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -8187,6 +8258,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8315,6 +8387,14 @@ "requires-port": "^1.0.0" } }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -8653,6 +8733,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -9817,6 +9898,28 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@cspotcode/source-map-support": { + "version": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -10584,6 +10687,30 @@ "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", "dev": true }, + "@tsconfig/node10": { + "version": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "optional": true + }, + "@tsconfig/node12": { + "version": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true + }, + "@tsconfig/node14": { + "version": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true + }, + "@tsconfig/node16": { + "version": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true + }, "@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -10695,6 +10822,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, + "peer": true, "requires": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -10711,6 +10839,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.2.tgz", "integrity": "sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw==", "dev": true, + "peer": true, "requires": { "undici-types": "~5.26.4" } @@ -10767,7 +10896,8 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true + "dev": true, + "peer": true }, "acorn-globals": { "version": "7.0.1", @@ -11055,6 +11185,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -11230,6 +11361,12 @@ "browserslist": "^4.22.1" } }, + "create-require": { + "version": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -11984,6 +12121,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.1.tgz", "integrity": "sha512-Nirw5B4nn69rVUZtemCQhwxOBhm0nsp3hmtF4rzCeWD7BkjAXRIji7xWQfnTNbz9g0aVsBX6aZK3n+23LM6uDw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.6.1", "@jest/types": "^29.6.1", @@ -13969,6 +14107,7 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", "dev": true, + "peer": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -14251,6 +14390,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, + "peer": true, "requires": { "fsevents": "~2.3.2" } @@ -14619,7 +14759,8 @@ "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", - "dev": true + "dev": true, + "peer": true }, "uc.micro": { "version": "1.0.6", @@ -14699,6 +14840,12 @@ "requires-port": "^1.0.0" } }, + "v8-compile-cache-lib": { + "version": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", diff --git a/keeperapi/package.json b/keeperapi/package.json index 19834de6..01b422e4 100644 --- a/keeperapi/package.json +++ b/keeperapi/package.json @@ -17,7 +17,7 @@ "test": "jest", "types": "tsc --watch", "types:ci": "tsc", - "prepublishOnly": "rollup -c && cp src/proto.d.ts dist", + "prepublishOnly": "rollup -c && node ./scripts/copyProtoDts.js", "publish-to-npm": "npm publish" }, "dependencies": { diff --git a/keeperapi/scripts/copyProtoDts.js b/keeperapi/scripts/copyProtoDts.js new file mode 100644 index 00000000..e93e35c3 --- /dev/null +++ b/keeperapi/scripts/copyProtoDts.js @@ -0,0 +1,15 @@ +const fs = require('fs') +const path = require('path') + +const src = path.resolve(__dirname, '../src/proto.d.ts') +const distDir = path.resolve(__dirname, '../dist') +const dst = path.resolve(distDir, 'proto.d.ts') + +if (!fs.existsSync(src)) { + throw new Error(`Source file not found: ${src}`) +} + +fs.mkdirSync(distDir, { recursive: true }) +fs.copyFileSync(src, dst) + +console.log(`Copied ${src} -> ${dst}`) diff --git a/shellcomponent/.gitignore b/shellcomponent/.gitignore new file mode 100644 index 00000000..a40b3a47 --- /dev/null +++ b/shellcomponent/.gitignore @@ -0,0 +1,5 @@ +.env.local +**/node_modules +**/dist +**/build +**/coverage diff --git a/shellcomponent/README.md b/shellcomponent/README.md new file mode 100644 index 00000000..acd7f16b --- /dev/null +++ b/shellcomponent/README.md @@ -0,0 +1,116 @@ +# Keeper shell component (`@keeper-security/keeper-shell-component`) + +Web component that embeds the Keeper CLI in the browser: **``** and **``** (same behavior, two tag names). The default mode runs the in-browser Keeper JavaScript SDK and xterm.js; you can optionally point CLI traffic at your own HTTP relay. + +## Requirements + +- A modern browser with **custom elements** and **shadow DOM**. +- Serve the app over **HTTP(S)**. ES modules and the SDK do not run from `file://`. + +## Install + +```bash +npm install @keeper-security/keeper-shell-component +``` + +In this monorepo you can depend on the package with a **file** URL, for example `"file:../shellcomponent"`. Run **`npm run build`** in `shellcomponent` first so **`dist/`** exists for consumers that resolve the published **`exports`** entry. + +## Register the custom elements + +Import the package **once** before relying on `` or `` in the DOM. The entry module applies the **`buffer`** shim and registers both tags. + +```ts +import "@keeper-security/keeper-shell-component"; +``` + +TypeScript types are exposed via **`keeper-shell.d.ts`** (see **`package.json` → `exports`**). + +## Use in HTML + +```html + + + +``` + +Equivalent tag: + +```html + +``` + +## Use in a bundled app (Vite, webpack, etc.) + +```ts +// e.g. main.ts or app entry +import "@keeper-security/keeper-shell-component"; +``` + +Then render the tag from your framework or template. Example with JSX (tag name must be a string so the browser upgrades it to the custom element): + +```tsx +export function Page() { + return ( + + ); +} +``` + +If TypeScript complains about unknown intrinsic elements, extend **`JSX.IntrinsicElements`** for **`"web-console"`** / **`"keeper-shell"`**, or create the element with **`document.createElement("web-console")`** and attach it in a ref. + +## Attributes + +| Attribute | Description | +|-----------|-------------| +| **`height`** | Height of the terminal region (e.g. `360px`, `24rem`). Default **`320px`**. | +| **`collapsed`** | If present, the terminal panel starts hidden; only the shell toggle control is shown until the user opens it. | +| **`embed`** | If present, renders a full in-page terminal **without** the open/hide control. | +| **`remote`** | If present, the CLI uses HTTP (`POST` to **`${apiBase}/cli`**, etc.) instead of the in-browser SDK. | +| **`api-base`** | Base URL for the remote CLI (no trailing slash). Default **`/api`** when **`remote`** is set. | +| **`keeper-host`** | Optional Keeper vault / region host override when using the in-browser SDK. | +| **`mask-input`** | If present, new prompts start with masked input (`*`); **Ctrl+O** toggles masking in the shell. | + +Boolean attributes follow HTML rules: include the attribute name to enable, omit to disable. + +## Programmatic API (optional) + +The package also exports helpers used by the shell (CLI dispatch, completion, vault helpers). See **`keeper-shell.d.ts`** and **`src/index.ts`** for **`dispatchCliLine`**, **`completeCliLine`**, **`setShellCliContext`**, **`resetShellVault`**, **`loginWithCredentials`**, and the **`KeeperShell`** / **`WebConsoleElement`** classes. + +## Layout note (non-embed) + +When the open/hide chrome is shown (**not** **`embed`**), the toggle is **`position: fixed`** at the **bottom-left** of the viewport (`12px` from edges). When the panel is visible, extra bottom padding is reserved so the terminal is not covered. Avoid placing critical fixed UI in that corner. + +## UMD bundle + +**`dist/keeper-shell.umd.cjs`** is built for environments that expect a UMD script. Wire it according to your host page’s script loader; the ESM path **`dist/keeper-shell.es.js`** is the primary **`import`** target. + +## Local development + +From **`shellcomponent/`**: + +```bash +npm install +npm run dev +``` + +Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell and optional dev-only fetch logging. + +```bash +npm run build +``` + +Produces **`dist/keeper-shell.es.js`**, **`dist/keeper-shell.umd.cjs`**, and emitted assets (for example the console toggle image if present). + +## Security and networking + +- **In-browser SDK** mode performs Keeper API calls from the user’s browser; your page’s origin, CSP, and CORS must allow what the SDK needs. +- **`remote`** mode sends CLI lines to **`api-base`**; only point it at backends you trust and that enforce auth appropriately. + +## License + +See **`package.json`** (`license` field). diff --git a/shellcomponent/index.html b/shellcomponent/index.html new file mode 100644 index 00000000..332ee899 --- /dev/null +++ b/shellcomponent/index.html @@ -0,0 +1,32 @@ + + + + + + Web console (dev) + + + +

Web console (dev)

+ + + + diff --git a/shellcomponent/keeper-shell.d.ts b/shellcomponent/keeper-shell.d.ts new file mode 100644 index 00000000..a65c7c80 --- /dev/null +++ b/shellcomponent/keeper-shell.d.ts @@ -0,0 +1,56 @@ +export type CliResult = { + code: number; + out: string; + err: string; + needPassword?: boolean; + loginUsername?: string; +}; + +export type ShellCliContext = { + keeperHost?: string; +}; + +export const KEEPER_SHELL_TAG: "keeper-shell"; +export const WEB_CONSOLE_TAG: "web-console"; + +export class KeeperShell extends HTMLElement { + /** Base URL when {@link remote} is true (default `/api`). */ + apiBase: string; + keeperHost: string; + collapsed: boolean; + maskInput: boolean; + /** + * When true, CLI uses HTTP (`POST ${apiBase}/cli`, …). + * When false (default), runs in-browser (Keeper SDK). + */ + remote: boolean; + /** + * @deprecated Same as `!remote`. Setting `local` removes `remote`; clearing `local` sets `remote`. + */ + local: boolean; + /** Full-page terminal; no Open/Hide console button. */ + embed: boolean; +} + +/** `` — extends {@link KeeperShell} (distinct class for dual registration). */ +export class WebConsoleElement extends KeeperShell {} + +/** Alias for {@link WebConsoleElement}. */ +export type WebConsole = WebConsoleElement; + +export function dispatchCliLine(line: string): Promise; +export function completeCliLine(line: string): { + base: string; + candidates: string[]; +}; + +export function setShellCliContext(next: ShellCliContext): void; +export function resetShellVault(): void; +export function loginWithCredentials(username: string, password: string): Promise; + +declare global { + interface HTMLElementTagNameMap { + "keeper-shell": KeeperShell; + "web-console": WebConsole; + } +} diff --git a/shellcomponent/package-lock.json b/shellcomponent/package-lock.json new file mode 100644 index 00000000..6c84bd94 --- /dev/null +++ b/shellcomponent/package-lock.json @@ -0,0 +1,1379 @@ +{ + "name": "@keeper-security/keeper-shell-component", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@keeper-security/keeper-shell-component", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@keeper-security/keeper-sdk-javascript": "file:../KeeperSdk", + "@keeper-security/keeperapi": "file:../keeperapi", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "protobufjs": "^7.2.4" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "buffer": "^6.0.3", + "path-browserify": "^1.0.1", + "typescript": "~5.7.2", + "vite": "^6.0.3" + } + }, + "../keeperapi": { + "name": "@keeper-security/keeperapi", + "version": "17.1.2", + "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" + }, + "devDependencies": { + "@babel/preset-env": "^7.23.5", + "@babel/preset-typescript": "^7.23.3", + "@rollup/plugin-commonjs": "^22.0.1", + "@rollup/plugin-node-resolve": "^13.3.0", + "@types/jest": "^24.0.15", + "@types/node": "^20.9.1", + "jest": "^29.6.1", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.8.1", + "protobufjs": "^7.2.4", + "protobufjs-cli": "^1.1.1", + "rollup": "^2.79.2", + "rollup-plugin-typescript2": "^0.32.1", + "ts-jest": "^29.1.1", + "ts-node": "^8.10.2", + "typescript": "^4.0.1" + } + }, + "../KeeperSdk": { + "name": "@keeper-security/keeper-sdk-javascript", + "version": "1.0.0", + "dependencies": { + "@keeper-security/keeperapi": "file:../keeperapi", + "typescript": "^4.6.3" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "prettier": "^3.8.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@keeper-security/keeper-sdk-javascript": { + "resolved": "../KeeperSdk", + "link": true + }, + "node_modules/@keeper-security/keeperapi": { + "resolved": "../keeperapi", + "link": true + }, + "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.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "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.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "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/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "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": "BSD-3-Clause" + }, + "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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "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.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/shellcomponent/package.json b/shellcomponent/package.json new file mode 100644 index 00000000..7680bbd8 --- /dev/null +++ b/shellcomponent/package.json @@ -0,0 +1,49 @@ +{ + "name": "@keeper-security/keeper-shell-component", + "version": "1.0.0", + "description": "Keeper web console: / + CLI + Keeper JavaScript SDK (in-browser); optional remote + api-base for your own HTTP relay.", + "type": "module", + "main": "./dist/keeper-shell.es.js", + "module": "./dist/keeper-shell.es.js", + "types": "./keeper-shell.d.ts", + "exports": { + ".": { + "types": "./keeper-shell.d.ts", + "import": "./dist/keeper-shell.es.js", + "require": "./dist/keeper-shell.umd.cjs" + } + }, + "files": [ + "dist", + "keeper-shell.d.ts" + ], + "scripts": { + "build:local-keeper-sdk": "npm --prefix ../KeeperSdk install && npm --prefix ../KeeperSdk run build", + "dev": "npm install && vite", + "dev:inspect": "npm install && node --inspect=9229 ./node_modules/vite/bin/vite.js", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@keeper-security/keeper-sdk-javascript": "file:../KeeperSdk", + "@keeper-security/keeperapi": "file:../keeperapi", + "protobufjs": "^7.2.4", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "buffer": "^6.0.3", + "path-browserify": "^1.0.1", + "typescript": "~5.7.2", + "vite": "^6.0.3" + }, + "keywords": [ + "keeper", + "web-components", + "cli", + "xterm" + ], + "license": "ISC" +} diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts new file mode 100644 index 00000000..899a7173 --- /dev/null +++ b/shellcomponent/src/KeeperShell.ts @@ -0,0 +1,1141 @@ +import { Terminal } from "@xterm/xterm"; +import { FitAddon } from "@xterm/addon-fit"; +import xtermCss from "@xterm/xterm/css/xterm.css?inline"; + +import { completeCliLine } from "./cli/cliComplete.js"; +import { dispatchCliLine } from "./cli/cliDispatch.js"; +import { setShellCliContext } from "./cli/cliContext.js"; +import { loginWithCredentials, resetShellVault } from "./cli/keeperCommands.js"; + +export const KEEPER_SHELL_TAG = "keeper-shell"; +/** Legacy custom element name (same behavior as {@link KEEPER_SHELL_TAG}). */ +export const WEB_CONSOLE_TAG = "web-console"; + +const ATTR_API_BASE = "api-base"; +const ATTR_KEEPER_HOST = "keeper-host"; +const ATTR_COLLAPSED = "collapsed"; +const ATTR_HEIGHT = "height"; +/** When set, CLI uses HTTP (POST `${apiBase}/cli`, …). Omitted = in-browser Keeper SDK (default for prod FE). */ +const ATTR_REMOTE = "remote"; +/** @deprecated Prefer default in-browser mode; `local` removes the `remote` attribute when set via property. */ +const ATTR_LOCAL = "local"; +/** Full in-page terminal only (no show/hide button). */ +const ATTR_EMBED = "embed"; +/** When set, each new `$ ` prompt starts with masked input (`*` display). Toggle with Ctrl+O. */ +const ATTR_MASK_INPUT = "mask-input"; + +/** Same as legacy ``: default `/api` when attribute is missing. */ +function normalizeApiBase(raw: string | null): string { + const s = (raw ?? "/api").trim() || "/api"; + return s.replace(/\/$/, "") || "/api"; +} + +/** Resolved at build time; change `../icon.jpg` if the asset moves. */ +const CONSOLE_TOGGLE_ICON_URL = new URL("../icon.jpg", import.meta.url).href; + +/** Same bitmap for both states; `aria-label` / `title` reflect expand vs collapse. */ +function setConsoleToggleButtonUi(btn: HTMLButtonElement, collapsed: boolean): void { + const label = collapsed ? "Open console" : "Hide console"; + btn.setAttribute("aria-label", label); + btn.title = label; + btn.innerHTML = ``; +} + +type CliResponse = { + out?: string; + err?: string; + error?: string; + code?: number; + needPassword?: boolean; + loginUsername?: string; +}; + +type CompleteResponse = { base?: unknown; candidates?: unknown }; + +const REMOTE_ERROR_BODY_MAX = 2000; + +/** Parse JSON from a remote CLI response; otherwise return a text preview (e.g. HTML 500 page). */ +async function readRemoteJsonBody(res: Response): Promise< + { kind: "json"; data: T } | { kind: "plain"; preview: string } +> { + const raw = await res.text(); + const t = raw.trim(); + if (t.length === 0) { + return { kind: "json", data: {} as T }; + } + try { + return { kind: "json", data: JSON.parse(t) as T }; + } catch { + const preview = + raw.length > REMOTE_ERROR_BODY_MAX ? raw.slice(0, REMOTE_ERROR_BODY_MAX) + "\n… (truncated)" : raw; + return { kind: "plain", preview }; + } +} + +const JSON_HEADERS = { + "Content-Type": "application/json", + Accept: "application/json", +} as const; + +/** JSON body for POST …/cli and …/cli/complete (`command` mirrors common server field names). */ +function remoteCliExecuteBody(line: string): string { + return JSON.stringify({ line, command: line }); +} + +function formatErr(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +function longestCommonPrefix(values: string[]): string { + if (values.length === 0) return ""; + let pref = values[0]; + for (let i = 1; i < values.length; i++) { + const s = values[i]; + let j = 0; + const n = Math.min(pref.length, s.length); + while (j < n && pref[j] === s[j]) j++; + pref = pref.slice(0, j); + if (pref === "") break; + } + return pref; +} + +/** Parsed stdin tokens from xterm (arrow keys arrive as ESC sequences). */ +type InputTok = + | { k: "c"; v: string } + | { k: "up" } + | { k: "down" } + | { k: "left" } + | { k: "right" } + | { k: "del" }; + +function feedInput(chunk: string, carry: { s: string }): InputTok[] { + const s = carry.s + chunk; + carry.s = ""; + const out: InputTok[] = []; + let i = 0; + while (i < s.length) { + if (s.charCodeAt(i) !== 0x1b) { + out.push({ k: "c", v: s[i] }); + i++; + continue; + } + if (i + 1 >= s.length) { + carry.s = s.slice(i); + break; + } + const c1 = s[i + 1]; + if (c1 === "[") { + let j = i + 2; + let foundFinal = false; + while (j < s.length) { + const code = s.charCodeAt(j); + if (code >= 0x40 && code <= 0x7e) { + foundFinal = true; + const fn = s[j]; + const param = s.slice(i + 2, j); + if (fn === "A") out.push({ k: "up" }); + else if (fn === "B") out.push({ k: "down" }); + else if (fn === "C") out.push({ k: "right" }); + else if (fn === "D") out.push({ k: "left" }); + else if (fn === "~" && param === "3") out.push({ k: "del" }); + i = j + 1; + break; + } + j++; + } + if (!foundFinal) { + carry.s = s.slice(i); + break; + } + continue; + } + if (c1 === "O") { + if (i + 2 >= s.length) { + carry.s = s.slice(i); + break; + } + const c2 = s[i + 2]; + if (c2 === "A") out.push({ k: "up" }); + else if (c2 === "B") out.push({ k: "down" }); + else if (c2 === "C") out.push({ k: "right" }); + else if (c2 === "D") out.push({ k: "left" }); + else { + out.push({ k: "c", v: "\x1b" }); + i++; + continue; + } + i += 3; + continue; + } + out.push({ k: "c", v: "\x1b" }); + i++; + } + return out; +} + +export class KeeperShell extends HTMLElement { + static observedAttributes = [ + ATTR_API_BASE, + ATTR_KEEPER_HOST, + ATTR_COLLAPSED, + ATTR_HEIGHT, + ATTR_REMOTE, + ATTR_LOCAL, + ATTR_EMBED, + ATTR_MASK_INPUT, + ]; + + private _term: Terminal | null = null; + private _fit: FitAddon | null = null; + private _ro: ResizeObserver | null = null; + private _onWinResize: (() => void) | null = null; + private _chain: Promise = Promise.resolve(); + private _lineBuf = ""; + private _started = false; + private _completing = false; + private _inputListeners: AbortController | null = null; + + constructor() { + super(); + // Lets the browser delegate focus to the xterm textarea inside shadow (keyboard input). + this.attachShadow({ mode: "open", delegatesFocus: true }); + } + + /** + * Base URL for CLI HTTP transport (no trailing slash). POST `${apiBase}/cli`, etc. + * Default `/api` when the attribute is omitted (legacy web-console behavior). + */ + get apiBase(): string { + return normalizeApiBase(this.getAttribute(ATTR_API_BASE)); + } + + set apiBase(v: string) { + this.setAttribute(ATTR_API_BASE, (v && v.trim()) || "/api"); + } + + get keeperHost(): string { + return (this.getAttribute(ATTR_KEEPER_HOST) || "").trim(); + } + + set keeperHost(v: string) { + if (!v.trim()) this.removeAttribute(ATTR_KEEPER_HOST); + else this.setAttribute(ATTR_KEEPER_HOST, v); + } + + get collapsed(): boolean { + return this.hasAttribute(ATTR_COLLAPSED); + } + + set collapsed(v: boolean) { + if (v) this.setAttribute(ATTR_COLLAPSED, ""); + else this.removeAttribute(ATTR_COLLAPSED); + } + + get maskInput(): boolean { + return this.hasAttribute(ATTR_MASK_INPUT); + } + + set maskInput(v: boolean) { + if (v) this.setAttribute(ATTR_MASK_INPUT, ""); + else this.removeAttribute(ATTR_MASK_INPUT); + } + + /** + * When set, CLI uses HTTP: POST `${apiBase}/cli`, etc. + * When omitted (default), input is handled in-browser (parser + Keeper SDK). + */ + get remote(): boolean { + return this.hasAttribute(ATTR_REMOTE); + } + + set remote(v: boolean) { + if (v) this.setAttribute(ATTR_REMOTE, ""); + else this.removeAttribute(ATTR_REMOTE); + } + + /** + * @deprecated Use {@link remote} instead. `local === !remote` (setting `local` removes `remote`). + */ + get local(): boolean { + return !this.remote; + } + + set local(v: boolean) { + if (v) this.removeAttribute(ATTR_REMOTE); + else this.setAttribute(ATTR_REMOTE, ""); + } + + /** Browser CLI layout: terminal fills the element; no collapsible chrome. */ + get embed(): boolean { + return this.hasAttribute(ATTR_EMBED); + } + + set embed(v: boolean) { + if (v) this.setAttribute(ATTR_EMBED, ""); + else this.removeAttribute(ATTR_EMBED); + } + + private _isLocal(): boolean { + return !this.hasAttribute(ATTR_REMOTE); + } + + private _isEmbed(): boolean { + return this.hasAttribute(ATTR_EMBED); + } + + /** `null` → in-browser CLI; else HTTP prefix (default `/api`). */ + private _remoteApiPrefix(): string | null { + if (this._isLocal()) return null; + return normalizeApiBase(this.getAttribute(ATTR_API_BASE)); + } + + private _syncShellContext(): void { + const h = this.getAttribute(ATTR_KEEPER_HOST)?.trim(); + setShellCliContext({ keeperHost: h || undefined }); + if (this._isLocal()) { + resetShellVault(); + } + } + + attributeChangedCallback(name: string, _old: string | null, val: string | null): void { + if (name === ATTR_HEIGHT && this.shadowRoot && !this._isEmbed()) { + const host = this.shadowRoot.querySelector(".wc-terminal-host"); + if (host instanceof HTMLElement) host.style.height = val || "320px"; + } + if (name === ATTR_KEEPER_HOST) { + this._syncShellContext(); + } + if (name === ATTR_EMBED || name === ATTR_LOCAL || name === ATTR_REMOTE) { + if (name === ATTR_LOCAL || name === ATTR_REMOTE) this._syncShellContext(); + const shouldShow = this._isEmbed() || !this.collapsed; + if (this._started) this._teardownTerminal(); + this._renderShell(); + this._wireChrome(); + if (shouldShow) this._mountTerminal(); + return; + } + if (name === ATTR_COLLAPSED && !this._isEmbed() && this.shadowRoot) { + const panel = this.shadowRoot.querySelector(".wc-panel"); + const btn = this.shadowRoot.querySelector(".wc-toggle"); + if (this.collapsed) { + panel?.setAttribute("hidden", ""); + if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, true); + this._teardownTerminal(); + } else { + panel?.removeAttribute("hidden"); + if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, false); + this._mountTerminal(); + } + this._applyHostShellMinHeight(); + } + } + + connectedCallback(): void { + if (!this.shadowRoot) return; + while (this.firstChild) { + this.removeChild(this.firstChild); + } + // Do not make the host a tab stop — xterm's inner textarea (`.wc-terminal-host`, tabindex=0) must receive keys. + this.tabIndex = -1; + this.setAttribute("role", "application"); + this.setAttribute("aria-label", "Keeper CLI"); + this._syncShellContext(); + this._renderShell(); + this._wireChrome(); + if (this._isEmbed() || !this.collapsed) { + this._mountTerminal(); + } + } + + disconnectedCallback(): void { + this._teardownTerminal(); + } + + private _wireChrome(): void { + if (!this.shadowRoot || this._isEmbed()) return; + const btn = this.shadowRoot.querySelector(".wc-toggle"); + if (!(btn instanceof HTMLButtonElement)) return; + btn.addEventListener("click", () => this._toggle()); + } + + private _toggle(): void { + if (this._isEmbed()) return; + const panel = this.shadowRoot?.querySelector(".wc-panel"); + const btn = this.shadowRoot?.querySelector(".wc-toggle"); + if (!(panel instanceof HTMLElement) || !(btn instanceof HTMLButtonElement)) return; + const hidden = panel.hasAttribute("hidden"); + if (hidden) { + panel.removeAttribute("hidden"); + setConsoleToggleButtonUi(btn, false); + this._mountTerminal(); + } else { + panel.setAttribute("hidden", ""); + setConsoleToggleButtonUi(btn, true); + this._teardownTerminal(); + } + this._applyHostShellMinHeight(); + } + + /** When the panel is hidden, avoid reserving terminal height so the page stays compact. */ + private _applyHostShellMinHeight(): void { + if (this._isEmbed()) return; + const panel = this.shadowRoot?.querySelector(".wc-panel"); + const panelHidden = panel?.hasAttribute("hidden"); + const h = (this.getAttribute(ATTR_HEIGHT) || "320px").trim(); + if (panelHidden) { + this.style.minHeight = "auto"; + return; + } + if (/^\d+(\.\d+)?px$/.test(h) || /^\d+(\.\d+)?rem$/.test(h)) { + this.style.minHeight = `calc(${h} + 3.75rem)`; + } else { + this.style.minHeight = "min(90vh, 28rem)"; + } + } + + private _renderShell(): void { + const height = this.getAttribute(ATTR_HEIGHT) || "320px"; + if (!this.shadowRoot) return; + this.shadowRoot.innerHTML = ""; + + const style = document.createElement("style"); + const embed = this._isEmbed(); + + if (embed) { + style.textContent = ` + ${xtermCss} + :host { + display: flex; + flex-direction: column; + width: 100%; + min-height: ${height}; + box-sizing: border-box; + font-family: system-ui, sans-serif; + pointer-events: auto; + } + .wc-root { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + padding: 0; + pointer-events: auto; + } + .wc-panel { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + margin: 0; + pointer-events: auto; + border: 2px solid #3d3d3d; + border-radius: 0; + overflow: hidden; + background: #121212 !important; + background-color: #121212 !important; + } + .wc-cli-cap { + flex-shrink: 0; + padding: 6px 12px; + font-size: 12px; + font-weight: 600; + color: #c8c8c8; + background: #1e1e1e !important; + border-bottom: 1px solid #404040; + user-select: none; + } + .wc-terminal-host { + flex: 1; + min-height: ${height}; + min-width: 280px; + width: 100%; + box-sizing: border-box; + padding: 6px 0; + position: relative; + cursor: text; + outline: none; + background: #0b0b0b !important; + background-color: #0b0b0b !important; + pointer-events: auto; + } + .wc-terminal-host .xterm { height: 100%; background-color: #0b0b0b !important; pointer-events: auto; } + .wc-terminal-host .xterm-viewport { background-color: #0b0b0b !important; } + .wc-terminal-host .xterm-screen { background-color: #0b0b0b !important; } + `; + } else { + style.textContent = ` + ${xtermCss} + :host { + display: block; + width: 100%; + box-sizing: border-box; + font-family: system-ui, sans-serif; + min-height: calc(${height} + 3.75rem); + pointer-events: auto; + } + .wc-root { + padding: 12px; + display: flex; + flex-direction: column; + gap: 0; + pointer-events: auto; + } + .wc-root:has(.wc-panel:not([hidden])) { + padding-bottom: 48px; + } + .wc-toggle { + box-sizing: border-box; + width: 30px; + height: 30px; + padding: 0; + margin: 0; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 0; + cursor: pointer; + border-radius: 6px; + border: 1px solid #c4c4c4; + background: #f0f0f0; + color: #2a2a2a; + flex-shrink: 0; + position: fixed; + bottom: 12px; + left: 12px; + z-index: 10000; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); + } + .wc-toggle:hover { + background: #e4e4e4; + border-color: #a8a8a8; + } + .wc-toggle:focus-visible { + outline: 2px solid #2563eb; + outline-offset: 2px; + } + .wc-toggle-icon { + display: block; + width: 22px; + height: 22px; + object-fit: contain; + flex-shrink: 0; + pointer-events: none; + user-select: none; + } + .wc-panel { + margin-top: 0; + display: flex; + flex-direction: column; + border: 2px solid #3d3d3d; + border-radius: 10px; + overflow: hidden; + background: #121212 !important; + background-color: #121212 !important; + box-shadow: 0 2px 12px rgba(0,0,0,0.25); + pointer-events: auto; + } + .wc-panel[hidden] { display: none; } + .wc-cli-cap { + flex-shrink: 0; + padding: 6px 12px; + font-size: 12px; + font-weight: 600; + color: #c8c8c8; + background: #1e1e1e !important; + border-bottom: 1px solid #404040; + user-select: none; + } + .wc-terminal-host { + flex: 1; + min-height: ${height}; + min-width: 280px; + width: 100%; + box-sizing: border-box; + padding: 6px 0; + position: relative; + cursor: text; + outline: none; + background: #0b0b0b !important; + background-color: #0b0b0b !important; + pointer-events: auto; + } + .wc-terminal-host .xterm { height: 100%; background-color: #0b0b0b !important; pointer-events: auto; } + .wc-terminal-host .xterm-viewport { background-color: #0b0b0b !important; } + .wc-terminal-host .xterm-screen { background-color: #0b0b0b !important; } + `; + } + + const root = document.createElement("div"); + root.className = "wc-root"; + + const panel = document.createElement("div"); + panel.className = "wc-panel"; + if (!embed && this.collapsed) panel.setAttribute("hidden", ""); + + const cap = document.createElement("div"); + cap.className = "wc-cli-cap"; + cap.textContent = this.remote + ? `Keeper CLI — HTTP (${this.apiBase})` + : "Keeper CLI — in-browser SDK (click in the terminal, then type)"; + + const host = document.createElement("div"); + host.className = "wc-terminal-host"; + if (!embed) { + host.style.height = height; + } + host.style.backgroundColor = "#0b0b0b"; + + panel.appendChild(cap); + panel.appendChild(host); + + if (!embed) { + const toggle = document.createElement("button"); + toggle.type = "button"; + toggle.className = "wc-toggle"; + setConsoleToggleButtonUi(toggle, this.collapsed); + root.appendChild(panel); + root.appendChild(toggle); + } else { + root.appendChild(panel); + } + this.shadowRoot.append(style, root); + + if (!embed) { + this.style.display = "block"; + this.style.width = "100%"; + this.style.boxSizing = "border-box"; + this._applyHostShellMinHeight(); + } else { + this.style.minHeight = "100%"; + this.style.display = "block"; + this.style.width = "100%"; + this.style.boxSizing = "border-box"; + } + } + + private _mountTerminal(): void { + if (this._started) return; + const host = this.shadowRoot?.querySelector(".wc-terminal-host"); + if (!(host instanceof HTMLElement)) return; + + const term = new Terminal({ + allowTransparency: false, + cursorBlink: true, + convertEol: true, + fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', + fontSize: 14, + theme: { + background: "#0b0b0b", + foreground: "#eaeaea", + cursor: "#eaeaea", + }, + }); + + const fit = new FitAddon(); + term.loadAddon(fit); + term.open(host); + + const refit = (): void => { + try { + fit.fit(); + } catch { + /* layout not ready */ + } + }; + + /** xterm FitAddon yields 0×0 if the container has no layout box yet. */ + const ensureUsableGeometry = (): void => { + refit(); + if (term.cols < 2 || term.rows < 1) { + try { + term.resize(80, 24); + } catch { + /* ignore */ + } + } + }; + + ensureUsableGeometry(); + requestAnimationFrame(() => { + ensureUsableGeometry(); + requestAnimationFrame(() => { + ensureUsableGeometry(); + term.focus(); + }); + }); + + host.tabIndex = 0; + host.setAttribute("aria-label", "Command line"); + const focusTerm = (): void => { + term.focus(); + }; + this._inputListeners?.abort(); + this._inputListeners = new AbortController(); + const sig = this._inputListeners.signal; + host.addEventListener("pointerdown", focusTerm, { signal: sig }); + host.addEventListener("focusin", focusTerm, { signal: sig }); + this.addEventListener( + "focusin", + (ev) => { + if (ev.target === this) focusTerm(); + }, + { signal: sig } + ); + + this._term = term; + this._fit = fit; + this._started = true; + this._lineBuf = ""; + + const HISTORY_MAX = 500; + const history: string[] = []; + const inputCarry = { s: "" }; + let histFromEnd = -1; + let histDraft = ""; + let cursorPos = 0; + let maskSensitive = this.hasAttribute(ATTR_MASK_INPUT); + let pendingLoginUsername: string | null = null; + + const maskDisplayActive = (): boolean => + maskSensitive || pendingLoginUsername !== null; + + const resetMaskAfterPasswordFlow = (): void => { + maskSensitive = this.hasAttribute(ATTR_MASK_INPUT); + }; + + const afterNewPrompt = (): void => { + if (this.hasAttribute(ATTR_MASK_INPUT)) maskSensitive = true; + }; + + const writeFreshPrompt = (): void => { + term.write("$ "); + afterNewPrompt(); + }; + + const writePromptLine = (): void => { + const line = this._lineBuf; + if (cursorPos < 0) cursorPos = 0; + if (cursorPos > line.length) cursorPos = line.length; + const visible = maskDisplayActive() ? "*".repeat(line.length) : line; + term.write(`\r\x1b[2K$ ${visible}`); + const back = line.length - cursorPos; + if (back > 0) term.write(`\x1b[${back}D`); + }; + + const resetHistoryNav = (): void => { + histFromEnd = -1; + }; + + const bumpEditing = (): void => { + histFromEnd = -1; + }; + + const pushHistoryEntry = (cmd: string): void => { + if (!cmd) return; + history.push(cmd); + if (history.length > HISTORY_MAX) history.shift(); + }; + + const historyOlder = (): void => { + if (history.length === 0) { + term.write("\x07"); + return; + } + if (histFromEnd === -1) { + histDraft = this._lineBuf; + histFromEnd = 0; + } else if (histFromEnd < history.length - 1) { + histFromEnd++; + } else { + term.write("\x07"); + return; + } + this._lineBuf = history[history.length - 1 - histFromEnd] ?? ""; + cursorPos = this._lineBuf.length; + writePromptLine(); + }; + + const historyNewer = (): void => { + if (histFromEnd === -1) { + return; + } + if (histFromEnd > 0) { + histFromEnd--; + this._lineBuf = history[history.length - 1 - histFromEnd] ?? ""; + cursorPos = this._lineBuf.length; + writePromptLine(); + return; + } + histFromEnd = -1; + this._lineBuf = histDraft; + cursorPos = this._lineBuf.length; + writePromptLine(); + }; + + const runTabComplete = async (): Promise => { + if (pendingLoginUsername !== null) { + term.write("\x07"); + return; + } + if (this._completing) return; + this._completing = true; + try { + bumpEditing(); + const remote = this._remoteApiPrefix(); + let data: CompleteResponse; + if (remote === null) { + data = completeCliLine(this._lineBuf) as CompleteResponse; + } else { + const url = `${remote}/cli/complete`; + const res = await fetch(url, { + method: "POST", + headers: JSON_HEADERS, + body: remoteCliExecuteBody(this._lineBuf), + }); + const parsed = await readRemoteJsonBody(res); + if (parsed.kind === "plain") { + term.write("\x07"); + return; + } + data = parsed.data; + if (!res.ok) { + term.write("\x07"); + return; + } + } + const base = typeof data.base === "string" ? data.base : ""; + const raw = data.candidates; + const candidates = Array.isArray(raw) + ? raw.filter((x): x is string => typeof x === "string") + : []; + const partial = this._lineBuf.slice(base.length); + + if (candidates.length === 0) { + term.write("\x07"); + return; + } + if (candidates.length === 1) { + this._lineBuf = base + candidates[0]; + cursorPos = this._lineBuf.length; + writePromptLine(); + return; + } + const lcp = longestCommonPrefix(candidates); + if (lcp.length > partial.length) { + this._lineBuf = base + lcp; + cursorPos = this._lineBuf.length; + writePromptLine(); + return; + } + term.writeln(""); + term.writeln(`\x1b[90m${candidates.join(" ")}\x1b[0m`); + writePromptLine(); + } catch { + term.write("\x07"); + } finally { + this._completing = false; + } + }; + + const flushLine = async (): Promise => { + const cmd = this._lineBuf.trim(); + const skipHistory = maskDisplayActive(); + this._lineBuf = ""; + cursorPos = 0; + resetHistoryNav(); + histDraft = ""; + + if (pendingLoginUsername !== null) { + if (!cmd) { + term.writeln("\x1b[31mlogin: password required\x1b[0m"); + writeFreshPrompt(); + return; + } + const username = pendingLoginUsername; + pendingLoginUsername = null; + term.writeln("\x1b[90mSigning in…\x1b[0m"); + try { + const remote = this._remoteApiPrefix(); + let data: CliResponse; + if (remote === null) { + data = await loginWithCredentials(username, cmd); + } else { + const res = await fetch(`${remote}/cli/login`, { + method: "POST", + headers: JSON_HEADERS, + body: JSON.stringify({ username, password: cmd }), + }); + const parsed = await readRemoteJsonBody(res); + if (parsed.kind === "plain") { + term.write( + `\x1b[31mHTTP ${res.status}: response is not JSON (server error page or non-API response).\n${parsed.preview}\x1b[0m\n` + ); + resetMaskAfterPasswordFlow(); + writeFreshPrompt(); + return; + } + data = parsed.data; + if (!res.ok) { + const d = data as CliResponse & { message?: string }; + const parts = [d?.error, d?.err, d?.out, d?.message].filter( + (x): x is string => typeof x === "string" && x.trim().length > 0 + ); + const msg = parts.length > 0 ? parts.join("\n") : res.statusText || "request failed"; + term.write(`\x1b[31m${msg}\x1b[0m\n`); + resetMaskAfterPasswordFlow(); + writeFreshPrompt(); + return; + } + } + if (data.out) term.write(data.out); + if (data.err) term.write(`\x1b[31m${data.err}\x1b[0m`); + } catch (err) { + term.write(`\x1b[31mError: ${formatErr(err)}\x1b[0m`); + } + resetMaskAfterPasswordFlow(); + writeFreshPrompt(); + return; + } + + if (!cmd) { + writeFreshPrompt(); + return; + } + if (!skipHistory) pushHistoryEntry(cmd); + + try { + const remote = this._remoteApiPrefix(); + let data: CliResponse; + if (remote === null) { + data = await dispatchCliLine(cmd); + } else { + const res = await fetch(`${remote}/cli`, { + method: "POST", + headers: JSON_HEADERS, + body: remoteCliExecuteBody(cmd), + }); + const parsed = await readRemoteJsonBody(res); + if (parsed.kind === "plain") { + term.write( + `\x1b[31mHTTP ${res.status}: response is not JSON (server error page or non-API response).\n${parsed.preview}\x1b[0m\n` + ); + writeFreshPrompt(); + return; + } + data = parsed.data; + if (!res.ok) { + const d = data as CliResponse & { message?: string }; + const parts = [d?.error, d?.err, d?.out, d?.message].filter( + (x): x is string => typeof x === "string" && x.trim().length > 0 + ); + const msg = parts.length > 0 ? parts.join("\n") : res.statusText || "request failed"; + term.write(`\x1b[31m${msg}\x1b[0m\n`); + writeFreshPrompt(); + return; + } + } + if (data.needPassword === true && typeof data.loginUsername === "string") { + pendingLoginUsername = data.loginUsername; + maskSensitive = true; + term.writeln("\x1b[90mPassword (masked):\x1b[0m"); + writeFreshPrompt(); + return; + } + if (data.out) term.write(data.out); + if (data.err) term.write(`\x1b[31m${data.err}\x1b[0m`); + } catch (err) { + term.write(`\x1b[31mError: ${formatErr(err)}\x1b[0m`); + } + writeFreshPrompt(); + }; + + const handleDataChunk = async (data: string): Promise => { + const tokens = feedInput(data, inputCarry); + for (const tok of tokens) { + if (tok.k === "up") { + historyOlder(); + continue; + } + if (tok.k === "down") { + historyNewer(); + continue; + } + if (tok.k === "left") { + bumpEditing(); + if (cursorPos > 0) { + cursorPos--; + writePromptLine(); + } else { + term.write("\x07"); + } + continue; + } + if (tok.k === "right") { + bumpEditing(); + if (cursorPos < this._lineBuf.length) { + cursorPos++; + writePromptLine(); + } else { + term.write("\x07"); + } + continue; + } + if (tok.k === "del") { + bumpEditing(); + if (cursorPos < this._lineBuf.length) { + const line = this._lineBuf; + this._lineBuf = line.slice(0, cursorPos) + line.slice(cursorPos + 1); + writePromptLine(); + } else { + term.write("\x07"); + } + continue; + } + const ch = tok.v; + if (ch === "\r" || ch === "\n") { + term.write("\r\n"); + await flushLine(); + continue; + } + if (ch === "\u007f" || ch === "\b") { + bumpEditing(); + if (cursorPos > 0) { + const line = this._lineBuf; + this._lineBuf = line.slice(0, cursorPos - 1) + line.slice(cursorPos); + cursorPos--; + writePromptLine(); + } + continue; + } + if (ch === "\t") { + await runTabComplete(); + continue; + } + if (ch === "\x0f") { + if (pendingLoginUsername === null) { + maskSensitive = !maskSensitive; + } + writePromptLine(); + continue; + } + if (ch === "\x03") { + this._lineBuf = ""; + cursorPos = 0; + pendingLoginUsername = null; + resetHistoryNav(); + histDraft = ""; + term.write("^C\r\n"); + writeFreshPrompt(); + continue; + } + const code = ch.charCodeAt(0); + if (code < 32) continue; + bumpEditing(); + const line = this._lineBuf; + this._lineBuf = line.slice(0, cursorPos) + ch + line.slice(cursorPos); + cursorPos++; + writePromptLine(); + } + }; + + term.onData((data) => { + this._chain = this._chain.then(() => handleDataChunk(data)); + }); + + const remote = this._remoteApiPrefix(); + if (remote === null) { + term.writeln("\x1b[1mWeb console\x1b[0m — commands run in this browser (Keeper SDK + CLI)."); + term.writeln("Tab completion and masked password entry are handled locally."); + term.writeln("Optional: `keeper-host` attribute for vault region."); + } else { + term.writeln("\x1b[1mWeb console\x1b[0m — commands execute on your backend API."); + term.writeln( + "Transport: JSON POST { line, command } (same string) to " + `${this.apiBase}/cli` + ); + term.writeln( + "Tab completes commands (POST { line, command } to " + `${this.apiBase}/cli/complete).` + ); + } + term.writeln("Up / Down — history; Left / Right — move cursor; Delete — forward delete."); + term.writeln( + remote === null + ? "Ctrl+O — toggle masked input (* per character; processed locally; masked lines are not saved to history)." + : "Ctrl+O — toggle masked input (* per character; real text is sent to the API; masked lines are not saved to history)." + ); + writeFreshPrompt(); + term.focus(); + const lateFocus = (): void => { + try { + fit.fit(); + if (term.cols < 2 || term.rows < 1) { + term.resize(80, 24); + } + term.focus(); + } catch { + /* ignore */ + } + }; + setTimeout(lateFocus, 50); + setTimeout(lateFocus, 300); + + this._ro = new ResizeObserver(() => { + try { + fit.fit(); + if (term.cols < 2 || term.rows < 1) { + term.resize(80, 24); + } + } catch { + /* detached */ + } + }); + this._ro.observe(host); + + this._onWinResize = () => { + try { + fit.fit(); + if (term.cols < 2 || term.rows < 1) { + term.resize(80, 24); + } + } catch { + /* ignore */ + } + }; + window.addEventListener("resize", this._onWinResize); + } + + private _teardownTerminal(): void { + this._inputListeners?.abort(); + this._inputListeners = null; + if (this._ro && this.shadowRoot) { + const host = this.shadowRoot.querySelector(".wc-terminal-host"); + if (host) this._ro.unobserve(host); + this._ro.disconnect(); + } + this._ro = null; + if (this._onWinResize) { + window.removeEventListener("resize", this._onWinResize); + this._onWinResize = null; + } + if (this._term) { + this._term.dispose(); + this._term = null; + this._fit = null; + } + this._started = false; + this._chain = Promise.resolve(); + } +} + +/** + * Same behavior as {@link KeeperShell}; separate class because a custom element + * constructor may only be registered once per registry (two tags ⇒ two classes). + */ +export class WebConsoleElement extends KeeperShell {} + +if (!customElements.get(KEEPER_SHELL_TAG)) { + customElements.define(KEEPER_SHELL_TAG, KeeperShell); +} +if (!customElements.get(WEB_CONSOLE_TAG)) { + customElements.define(WEB_CONSOLE_TAG, WebConsoleElement); +} diff --git a/shellcomponent/src/bufferPolyfill.ts b/shellcomponent/src/bufferPolyfill.ts new file mode 100644 index 00000000..3fa88e59 --- /dev/null +++ b/shellcomponent/src/bufferPolyfill.ts @@ -0,0 +1,6 @@ +import { Buffer } from "buffer"; + +const g = globalThis as typeof globalThis & { Buffer?: typeof Buffer }; +if (typeof g.Buffer === "undefined") { + g.Buffer = Buffer; +} diff --git a/shellcomponent/src/cli/cliCommandDocs.ts b/shellcomponent/src/cli/cliCommandDocs.ts new file mode 100644 index 00000000..76806ca8 --- /dev/null +++ b/shellcomponent/src/cli/cliCommandDocs.ts @@ -0,0 +1,224 @@ +/** + * Long-form `--help` text per CLI command (keeper-shell + KeeperSdk). + */ + +const KEEPER_VAULT_SURFACE = ` +KeeperVault (JavaScript SDK) — operations available in code (not all exposed as CLI yet): + + Session: login, loginWithSessionToken, logout, resumeSession, sync, disconnect, registerDevice + Records: getRecords, findRecord, findRecords, getRecordByUid, getRecordsByType, + addRecord, updateRecord, deleteRecord, moveRecord, getRecordHistory, + printRecords + Sharing: shareRecord, removeRecordShare, getRecordShareInfo + Folders: listFolder, changeDirectory, getFolder, mkdir, addFolder, updateFolder, + renameFolder, deleteFolder, rmdir, tree, getCurrentFolderUid + Shared folders: getSharedFolders, listSharedFolders, shareFolder, … + Teams / metadata: getTeams, getRecordMetadata, getSummary, … + +Utilities exported from @keeper-security/keeper-sdk-javascript include searchRecords, +formatRecord, getRecordTitle, getRecordPassword, getRecordLogin, shareRecord, … +See the SDK package for full APIs. +`.trim(); + +const DOCS: Record = { + help: ` +help — show commands or short syntax for one command + +SYNOPSIS + help [COMMAND] + +DESCRIPTION + Without arguments, lists every built-in command with a one-line summary. + With COMMAND, prints the same overview line plus usage for that command. + + For full documentation on each command, run: + COMMAND --help + COMMAND -h + +OPTIONS + None. This command does not take GNU-style flags. + +SEE ALSO + Each command’s --help output. +`.trim(), + + login: ` +login — authenticate to Keeper (vault session) + +SYNOPSIS + login [--username|--user EMAIL_OR_NAME] + login [--username|--user U] [--session-token|--token|--st TOKEN] + login [--username|--user U] [--session-token TOKEN] [--session-token-plain] + +DESCRIPTION + Establishes a Keeper session using KeeperVault inside keeper-shell (in-memory + session storage in the browser). + + Username comes from --username / --user or KEEPER_USERNAME. + + Password MUST NOT appear on the CLI line (logging, proxies, browser history). + Automation: set KEEPER_PASSWORD in the environment when embedding in Node. + Web shell: run login with only a username; the UI prompts for a masked password + and sends it through the login transport, not in "line". + + Session token login uses KeeperVault.loginWithSessionToken. The token may be + passed on the command line or via KEEPER_SESSION_TOKEN (sensitive — same + caveats as any secret on argv). + + --session-token-plain encodes the token from UTF-8 to base64url before the + SDK call (same idea as the session_token_login example when the token is raw text). + + Device registration: session token login requires deviceToken + privateKey for + this host in session storage. Use register-device (or a prior password login in + this shell) to store them; see register-device --help. + +OPTIONS + --username, --user Account identifier (often email). + --session-token, --token, --st Session token string (or use KEEPER_SESSION_TOKEN). + --session-token-plain Treat --session-token value as plain UTF-8 and encode base64url. + +ENVIRONMENT + KEEPER_USERNAME Default username if not passed on the command line. + KEEPER_PASSWORD Password for non-interactive login (no session token). + KEEPER_SESSION_TOKEN Session token when not passed as a flag. + KEEPER_HOST Optional vault host / region (also: keeper-host attribute). + +KEEPER SDK + Uses KeeperVault.login or loginWithSessionToken, then sync. resumeSession and + clone-code flows exist in the SDK but are not exposed in this CLI yet. + +${KEEPER_VAULT_SURFACE} +`.trim(), + + logout: ` +logout — end the current Keeper session + +SYNOPSIS + logout + +DESCRIPTION + Calls KeeperVault.logout when a session exists. + +OPTIONS + None. + +${KEEPER_VAULT_SURFACE} +`.trim(), + + records: ` +records — list vault records (record UID and title) + +SYNOPSIS + records [list] + +DESCRIPTION + Runs sync, then prints a table of record_uid and title for each record. + +ARGUMENTS + list Optional; default behavior is to list. Other subcommands may be added later. + +OPTIONS + --help, -h Show this help. + +SESSION + If not logged in, the CLI attempts env-based login (KEEPER_USERNAME / + KEEPER_PASSWORD). Use the masked password flow in the shell if you rely on it. + +KEEPER SDK + Maps to KeeperVault.sync(), getRecords(), getRecordTitle(). + Related APIs you can extend in this CLI later: findRecords, getRecordsByType, + addRecord, updateRecord, deleteRecord, shareRecord, getRecordHistory, … + +${KEEPER_VAULT_SURFACE} +`.trim(), + + folders: ` +folders — list shared folders + +SYNOPSIS + folders [list] + +DESCRIPTION + Runs sync, then prints shared_folder_uid and name for each shared folder. + +ARGUMENTS + list Optional; default is list. + +OPTIONS + --help, -h Show this help. + +SESSION + Same as records: uses env login if needed, or log in via the shell first. + +KEEPER SDK + Uses KeeperVault.sync(), getSharedFolders(). + Related: listSharedFolders, shareFolder, FolderManager / SharedFolderManager. + +${KEEPER_VAULT_SURFACE} +`.trim(), + + mkdir: ` +mkdir — host filesystem directory (disabled in embedded shell) + +SYNOPSIS + mkdir [-p|--parents] [--] RELATIVE_PATH + +DESCRIPTION + In keeper-shell with in-browser SDK transport, host mkdir is not available (no + sandboxed filesystem). Set api-base to an HTTP CLI backend if you need + this command, or use Keeper vault folder APIs in code. + +OPTIONS + -p, --parents (remote server only.) + -- End of options. + +NOTE + Vault folder operations live on KeeperVault (mkdir, addFolder, …) in the SDK. +`.trim(), + + "register-device": ` +register-device — store device token and private key for session-token login + +SYNOPSIS + register-device --device-token|--dt B64 --private-key|--pk B64 [--username|--user U] + +DESCRIPTION + Calls KeeperVault.registerDevice to save device credentials for the current + host in this shell’s in-memory session. After this, you can run: + + login --username YOU --session-token TOKEN + + without a prior password login in this shell, as long as the token is valid. + + Obtain device_token and private_key from another machine’s keeper config after + a successful login, or from your integration that provisions device keys. + Values accept base64 or base64url (same decoding as SessionManager / normal64Bytes). + +OPTIONS + --device-token, --dt Device token string. + --private-key, --pk Device private key string. + --username, --user Optional; sets last username in session storage (recommended). + +ENVIRONMENT + REGISTER_DEVICE_TOKEN Same as --device-token when flag omitted. + REGISTER_DEVICE_PRIVATE_KEY Same as --private-key when flag omitted. + KEEPER_HOST Same as other keeper commands. + +KEEPER SDK + KeeperVault.registerDevice(deviceToken, privateKey, { username? }) + +${KEEPER_VAULT_SURFACE} +`.trim(), +}; + +/** Long help text for `COMMAND --help`; null if unknown. */ +export function getDetailedHelpPage(command: string): string | null { + const key = command.toLowerCase(); + const body = DOCS[key]; + if (!body) return null; + return `${body}\n`; +} + +export function listDocumentedCommands(): readonly string[] { + return Object.keys(DOCS).sort(); +} diff --git a/shellcomponent/src/cli/cliComplete.ts b/shellcomponent/src/cli/cliComplete.ts new file mode 100644 index 00000000..2540c866 --- /dev/null +++ b/shellcomponent/src/cli/cliComplete.ts @@ -0,0 +1,99 @@ +/** + * Tab-completion metadata for the keeper-shell CLI. + */ +import { CLI_TOP_LEVEL_NAMES } from "./cliHelp.js"; + +const TOP_LEVEL = CLI_TOP_LEVEL_NAMES; + +const SUBCOMMANDS: Record = { + records: ["list"], + folders: ["list"], +}; + +const HELP_FLAGS = ["--help", "-h"] as const; + +/** Long/short flags after a command (plus universal --help / -h). */ +const FLAG_OPTIONS: Record = { + login: [ + "--user", + "--username", + "--session-token", + "--token", + "--st", + "--session-token-plain", + ], + mkdir: ["-p", "--parents"], + "register-device": [ + "--device-token", + "--dt", + "--private-key", + "--pk", + "--user", + "--username", + ], +}; + +function flagsFor(cmd: string): readonly string[] { + const extra = FLAG_OPTIONS[cmd] ?? []; + return [...HELP_FLAGS, ...extra]; +} + +export type CliCompleteResult = { + /** Line prefix to keep; replacement segment is `line.slice(base.length)`. */ + base: string; + /** Suggested full tokens (each replaces the partial segment). */ + candidates: string[]; +}; + +/** + * @param line - Current input (cursor treated as end of line). + */ +export function completeCliLine(line: string): CliCompleteResult { + const completesNewWord = /\s$/.test(line); + const segments = line.match(/\S+/g) ?? []; + + let words: string[]; + let stub: string; + + if (completesNewWord) { + words = [...segments]; + stub = ""; + } else if (segments.length === 0) { + words = []; + stub = ""; + } else { + words = segments.slice(0, -1); + stub = segments[segments.length - 1] ?? ""; + } + + const lc = (s: string) => s.toLowerCase(); + const stubLc = lc(stub); + + const baseFor = (partialLen: number) => + partialLen > 0 ? line.slice(0, line.length - partialLen) : line; + + if (words.length === 0) { + const hits = TOP_LEVEL.filter((c) => c.startsWith(stubLc)); + return { base: baseFor(stub.length), candidates: hits }; + } + + const cmd = lc(words[0]); + + if (words.length === 1) { + if (stub.startsWith("-")) { + const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); + return { base: baseFor(stub.length), candidates: hits }; + } + const subs = SUBCOMMANDS[cmd]; + if (subs) { + const hits = subs.filter((s) => lc(s).startsWith(stubLc)); + return { base: baseFor(stub.length), candidates: hits }; + } + if (completesNewWord || stub.length > 0) { + const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); + return { base: baseFor(stub.length), candidates: hits }; + } + } + + return { base: line, candidates: [] }; +} diff --git a/shellcomponent/src/cli/cliContext.ts b/shellcomponent/src/cli/cliContext.ts new file mode 100644 index 00000000..5ef186b3 --- /dev/null +++ b/shellcomponent/src/cli/cliContext.ts @@ -0,0 +1,28 @@ +export type ShellCliContext = { + /** Optional Keeper host / region (overrides KEEPER_HOST when set). */ + keeperHost?: string; +}; + +let ctx: ShellCliContext = {}; + +export function setShellCliContext(next: ShellCliContext): void { + ctx = { ...next }; +} + +export function getShellKeeperHost(): string | undefined { + const fromCtx = ctx.keeperHost?.trim(); + if (fromCtx) return fromCtx; + if (typeof process !== "undefined" && process.env) { + const h = (process.env.KEEPER_HOST || "").trim(); + if (h) return h; + } + return undefined; +} + +export function envString(name: string): string | undefined { + if (typeof process !== "undefined" && process.env) { + const v = process.env[name]; + return typeof v === "string" && v.length > 0 ? v : undefined; + } + return undefined; +} diff --git a/shellcomponent/src/cli/cliDispatch.ts b/shellcomponent/src/cli/cliDispatch.ts new file mode 100644 index 00000000..9e9328b0 --- /dev/null +++ b/shellcomponent/src/cli/cliDispatch.ts @@ -0,0 +1,51 @@ +import { mkdirCommand } from "./mkdir.js"; +import { + keeperLoginCommand, + keeperLogoutCommand, + keeperRecordsCommand, + keeperFoldersCommand, + registerDeviceCommand, +} from "./keeperCommands.js"; +import { helpCommand } from "./cliHelp.js"; +import { tokenizeArguments, parseCliArgs } from "./cliParse.js"; +import type { CliResult } from "./types.js"; + +const rawMax = + typeof process !== "undefined" && process.env?.CLI_MAX_LINE_LENGTH + ? Number(process.env.CLI_MAX_LINE_LENGTH) + : NaN; +const MAX_LINE_LENGTH = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 8192; + +export async function dispatchCliLine(line: string): Promise { + if (line.length > MAX_LINE_LENGTH) { + return { code: 1, out: "", err: "line too long\n" }; + } + + const tokens = tokenizeArguments(line.trim()); + const name = tokens[0]?.toLowerCase(); + const rest = tokens.slice(1); + const parsed = parseCliArgs(rest); + + if (!name) { + return { code: 0, out: "", err: "" }; + } + + switch (name) { + case "help": + return helpCommand(parsed); + case "mkdir": + return mkdirCommand(parsed); + case "login": + return keeperLoginCommand(parsed); + case "logout": + return keeperLogoutCommand(parsed); + case "records": + return keeperRecordsCommand(parsed); + case "folders": + return keeperFoldersCommand(parsed); + case "register-device": + return registerDeviceCommand(parsed); + default: + return { code: 1, out: "", err: `Unknown command: ${name}\n` }; + } +} diff --git a/shellcomponent/src/cli/cliHelp.ts b/shellcomponent/src/cli/cliHelp.ts new file mode 100644 index 00000000..587de052 --- /dev/null +++ b/shellcomponent/src/cli/cliHelp.ts @@ -0,0 +1,99 @@ +import { wantsCliHelp, type ParsedCli } from "./cliParse.js"; +import { getDetailedHelpPage } from "./cliCommandDocs.js"; +import type { CliResult } from "./types.js"; + +type CliHelpEntry = { + name: string; + usage: string; + description: string; +}; + +const CLI_COMMANDS: CliHelpEntry[] = [ + { + name: "folders", + usage: "folders [list] [--help|-h]", + description: "List shared folders in the vault (uses env or `login --username …`).", + }, + { + name: "help", + usage: "help [command] (see also: help --help)", + description: "Show all commands, or full docs for one command (same as COMMAND --help).", + }, + { + name: "login", + usage: + "login [--username|--user ] [--session-token|--token|--st ] [--session-token-plain] [--help|-h]", + description: + "Log in with password (env / masked prompt) or session token (flag or KEEPER_SESSION_TOKEN). Password never on CLI line.", + }, + { + name: "logout", + usage: "logout [--help|-h]", + description: "Log out of the current Keeper session.", + }, + { + name: "mkdir", + usage: "mkdir [-p|--parents] [--] ; mkdir --help", + description: + "Host mkdir is disabled in keeper-shell (no filesystem). Use `api-base` for an HTTP CLI backend, or SDK folder APIs.", + }, + { + name: "records", + usage: "records [list] [--help|-h]", + description: "List vault records (uid and title).", + }, + { + name: "register-device", + usage: + "register-device --device-token|--dt --private-key|--pk [--username ] [--help|-h]", + description: + "Store device token + private key in this shell’s session so session-token login works (see login --help).", + }, +].sort((a, b) => a.name.localeCompare(b.name)); + +/** Top-level names for tab completion (sorted). */ +export const CLI_TOP_LEVEL_NAMES: readonly string[] = CLI_COMMANDS.map((c) => c.name); + +function formatAllCommands(): string { + const w = Math.max(...CLI_COMMANDS.map((c) => c.name.length), 8); + let out = "Supported commands:\n\n"; + for (const c of CLI_COMMANDS) { + out += ` ${c.name.padEnd(w)} ${c.description}\n`; + } + out += "\nOptions use GNU-style syntax: `--name`, `--name=value`, short flags (`-p` or `-rf` when each letter is a switch), and `--` ends options.\n"; + out += "Quoted arguments and `\\` escapes are supported.\n\n"; + out += "Run `help ` for a short summary, or `command --help` / `command -h` for full documentation.\n"; + return out; +} + +function formatOne(name: string): CliResult { + const key = name.toLowerCase(); + const long = getDetailedHelpPage(key); + if (long) { + return { code: 0, out: long, err: "" }; + } + const c = CLI_COMMANDS.find((e) => e.name === key); + if (!c) { + return { code: 1, out: "", err: `help: unknown command: ${name}\n` }; + } + const out = `${c.name} — ${c.description}\n Usage: ${c.usage}\n`; + return { code: 0, out, err: "" }; +} + +export function helpCommand(parsed: ParsedCli): CliResult { + if (wantsCliHelp(parsed)) { + const h = getDetailedHelpPage("help"); + return { code: 0, out: h ?? "", err: "" }; + } + if (parsed.opts.size > 0) { + return { code: 1, out: "", err: "help: unknown option (try `help --help`)\n" }; + } + const args = parsed.positional; + if (args.length === 0) { + return { code: 0, out: formatAllCommands(), err: "" }; + } + if (args.length > 1) { + return { code: 1, out: "", err: "Usage: help [command]\n" }; + } + return formatOne(args[0]); +} diff --git a/shellcomponent/src/cli/cliParse.ts b/shellcomponent/src/cli/cliParse.ts new file mode 100644 index 00000000..d46db5a5 --- /dev/null +++ b/shellcomponent/src/cli/cliParse.ts @@ -0,0 +1,174 @@ +/** + * Shell-like tokenization and GNU-style option parsing (similar in spirit to + * Keeper Commander / CommandLineParser on .NET: quoted args, `--`, `-abc`, `--key=value`). + */ + +export type ParsedCli = { + positional: string[]; + /** Lowercase option names; value `true` means boolean flag. */ + opts: Map; +}; + +const isWhitespace = (ch: string) => /\s/.test(ch); + +/** + * Split a command line into tokens; respects double quotes and `\` escapes. + * Mirrors the Commander Cli `TokenizeArguments` behavior in broad strokes. + */ +export function tokenizeArguments(args: string): string[] { + const out: string[] = []; + const sb: string[] = []; + let pos = 0; + let inQuote = false; + let escape = false; + + const flush = () => { + if (sb.length > 0) { + out.push(sb.join("")); + sb.length = 0; + } + }; + + while (pos < args.length) { + const ch = args[pos]; + if (escape) { + escape = false; + sb.push(ch); + pos++; + continue; + } + if (inQuote) { + if (ch === "\\") { + escape = true; + pos++; + continue; + } + if (ch === '"') { + inQuote = false; + pos++; + continue; + } + sb.push(ch); + pos++; + continue; + } + switch (ch) { + case "\\": + escape = true; + pos++; + break; + case '"': + inQuote = true; + pos++; + break; + default: + if (isWhitespace(ch)) { + flush(); + pos++; + } else { + sb.push(ch); + pos++; + } + } + } + flush(); + return out; +} + +function setBool(opts: Map, k: string): void { + opts.set(k.toLowerCase(), true); +} + +function setStr(opts: Map, k: string, v: string): void { + opts.set(k.toLowerCase(), v); +} + +/** + * Parse argv-style tokens after the command name: GNU-like long/short options and `--`. + */ +export function parseCliArgs(tokens: string[]): ParsedCli { + const positional: string[] = []; + const opts = new Map(); + + let i = 0; + while (i < tokens.length) { + const t = tokens[i]; + if (t === "--") { + positional.push(...tokens.slice(i + 1)); + break; + } + if (t === "-" || !t.startsWith("-")) { + positional.push(t); + i++; + continue; + } + + if (t.startsWith("--")) { + const body = t.slice(2); + if (!body) { + positional.push(t); + i++; + continue; + } + const eq = body.indexOf("="); + if (eq >= 0) { + setStr(opts, body.slice(0, eq), body.slice(eq + 1)); + i++; + continue; + } + const name = body; + const next = tokens[i + 1]; + if (next && next !== "--" && !next.startsWith("-")) { + setStr(opts, name, next); + i += 2; + continue; + } + setBool(opts, name); + i++; + continue; + } + + const rest = t.slice(1); + if (!rest) { + positional.push(t); + i++; + continue; + } + if (/^[A-Za-z]$/.test(rest)) { + setBool(opts, rest); + i++; + continue; + } + if (/^[A-Za-z]+$/.test(rest)) { + for (const ch of rest) setBool(opts, ch); + i++; + continue; + } + + positional.push(t); + i++; + } + + return { positional, opts }; +} + +export function hasOpt(opts: Map, ...names: string[]): boolean { + for (const n of names) { + const v = opts.get(n.toLowerCase()); + if (v === true) return true; + } + return false; +} + +export function getOpt(opts: Map, ...names: string[]): string | undefined { + for (const n of names) { + const v = opts.get(n.toLowerCase()); + if (v !== undefined && v !== true) return v; + } + return undefined; +} + +/** True if `-h` / `--help` was passed (boolean flags only). */ +export function wantsCliHelp(parsed: ParsedCli): boolean { + return hasOpt(parsed.opts, "help", "h"); +} diff --git a/shellcomponent/src/cli/inMemoryConfigLoader.ts b/shellcomponent/src/cli/inMemoryConfigLoader.ts new file mode 100644 index 00000000..5c589f48 --- /dev/null +++ b/shellcomponent/src/cli/inMemoryConfigLoader.ts @@ -0,0 +1,43 @@ +import type { ConfigLoader, KeeperJsonConfig } from "@keeper-security/keeper-sdk-javascript"; + +/** + * When `import.meta.env.DEV` is true and all three Vite env vars are set, preloads the same + * flat device shape as `~/.keeper/config.json` so {@link SessionManager.getDeviceConfig} returns + * a token/key before login — keeperapi `loginV3` then skips `registerDevice()`. + * Use the same username you pass to `login` as `VITE_KEEPER_DEV_DEVICE_USER` (case-insensitive match). + */ +function readDevSeededKeeperConfig(): KeeperJsonConfig { + if (import.meta.env?.DEV !== true) return {}; + const user = (import.meta.env.VITE_KEEPER_DEV_DEVICE_USER as string | undefined)?.trim(); + const device_token = (import.meta.env.VITE_KEEPER_DEV_DEVICE_TOKEN as string | undefined)?.trim(); + const private_key = (import.meta.env.VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY as string | undefined)?.trim(); + if (!user || !device_token || !private_key) return {}; + return { + user, + last_login: user, + device_token, + private_key, + }; +} + +/** + * Session/device storage for {@link KeeperVault} inside the browser shell (no `.keeper` on disk). + */ +export class InMemoryConfigLoader implements ConfigLoader { + public readonly configDir = ""; + + private data: KeeperJsonConfig; + + constructor() { + const seed = readDevSeededKeeperConfig(); + this.data = Object.keys(seed).length > 0 ? seed : {}; + } + + async load(): Promise { + return JSON.parse(JSON.stringify(this.data)) as KeeperJsonConfig; + } + + async save(config: KeeperJsonConfig): Promise { + this.data = JSON.parse(JSON.stringify(config)) as KeeperJsonConfig; + } +} diff --git a/shellcomponent/src/cli/keeperCommands.ts b/shellcomponent/src/cli/keeperCommands.ts new file mode 100644 index 00000000..d9b27f94 --- /dev/null +++ b/shellcomponent/src/cli/keeperCommands.ts @@ -0,0 +1,368 @@ +/** + * Maps parsed CLI lines to KeeperSdk (KeeperVault) — browser-friendly (no Node Buffer/path). + */ +import type { DRecord, DSharedFolder } from "@keeper-security/keeper-sdk-javascript"; +import * as KeeperSdk from "@keeper-security/keeper-sdk-javascript"; +import { getDetailedHelpPage } from "./cliCommandDocs.js"; +import { envString, getShellKeeperHost } from "./cliContext.js"; +import { InMemoryConfigLoader } from "./inMemoryConfigLoader.js"; +import { getOpt, hasOpt, wantsCliHelp, type ParsedCli } from "./cliParse.js"; +import type { CliResult } from "./types.js"; + +const { KeeperVault, LogLevel, SessionManager, getRecordTitle, SdkDefaults } = KeeperSdk; + +type VaultInstance = InstanceType; + +const LOGIN_OPT_NAMES = new Set([ + "username", + "user", + "session-token", + "token", + "st", + "session-token-plain", +]); + +let vault: VaultInstance | null = null; + +function utf8ToBase64Url(s: string): string { + const bytes = new TextEncoder().encode(s); + let bin = ""; + for (let i = 0; i < bytes.length; i++) { + bin += String.fromCharCode(bytes[i]!); + } + const b64 = btoa(bin); + return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function getVault(): VaultInstance { + if (!vault) { + const host = getShellKeeperHost(); + vault = new KeeperVault({ + ...(host ? { host } : {}), + useConsoleAuth: false, + logLevel: LogLevel.WARN, + sessionStorage: new SessionManager(new InMemoryConfigLoader()), + }); + if (import.meta.env?.DEV === true) { + const devDevice = + import.meta.env.VITE_KEEPER_DEV_DEVICE_USER && + import.meta.env.VITE_KEEPER_DEV_DEVICE_TOKEN && + import.meta.env.VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY; + console.info("[keeper-shell dev] KeeperVault created", { + host: host || "(SDK default; US prod host if unset)", + clientVersion: SdkDefaults.CLIENT_VERSION, + useConsoleAuth: false, + logLevel: "WARN", + preseededDeviceFromEnv: Boolean(devDevice), + }); + } + } + return vault; +} + +/** Reset vault singleton (e.g. after keeper-host attribute change). */ +export function resetShellVault(): void { + vault = null; +} + +function recordUid(rec: { uid?: string }): string { + return rec.uid || "(unknown uid)"; +} + +function errMsg(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +/** + * Enriches errors where the browser only reports "Failed to fetch" (no status/CORS detail). + */ +function formatKeeperClientError(context: string, e: unknown): string { + const base = errMsg(e); + const inBrowser = typeof document !== "undefined"; + if (!inBrowser) { + return `${context}: ${base}\n`; + } + const low = base.toLowerCase(); + const fetchish = + low.includes("failed to fetch") || + low.includes("networkerror") || + low.includes("load failed") || + low.includes("network request failed"); + if (!fetchish) { + return `${context}: ${base}\n`; + } + const host = getShellKeeperHost(); + const hostHint = host + ? `\n Region/host in use: ${host} — wrong region can look like a network error; adjust keeper-host or KEEPER_HOST if needed.` + : `\n Region/host: default production (US). EU/AU/CA/JP tenants often need keeper-host on or KEEPER_HOST.`; + return ( + `${context}: ${base}\n` + + ` Why only this text: browsers intentionally hide the underlying HTTP status / CORS detail for many failed requests.\n` + + ` Typical causes (your password may still be correct):\n` + + ` • CORS — Keeper’s API may not allow calls from this page’s origin (common for http://localhost in dev).\n` + + ` • Network — offline, DNS, VPN, corporate proxy, or firewall blocking HTTPS to Keeper.\n` + + ` • Mixed content — page is http while the API is https; load the dev page over https.\n` + + hostHint + + `\n Mitigation: run login from a backend your page trusts (set web-console remote + api-base), or use the SDK from Node.js.\n` + ); +} + +/** Shared login used by CLI and shell password transport (password never in `line`). */ +export async function loginWithCredentials(username: string, password: string): Promise { + try { + const v = getVault(); + if (v.isLoggedIn) { + await v.logout(); + } + await v.login(username, password); + await v.sync(); + return { code: 0, out: `keeper: logged in as ${username}.\n`, err: "" }; + } catch (e) { + return { code: 1, out: "", err: formatKeeperClientError("keeper", e) }; + } +} + +export async function loginWithSessionTokenCredentials( + username: string, + sessionToken: string, + options?: { plainToken?: boolean } +): Promise { + let token = sessionToken.trim(); + if (options?.plainToken && token.length > 0) { + token = utf8ToBase64Url(token); + } + try { + const v = getVault(); + if (v.isLoggedIn) { + await v.logout(); + } + await v.loginWithSessionToken(username, token); + await v.sync(); + return { code: 0, out: `keeper: logged in as ${username} (session token).\n`, err: "" }; + } catch (e) { + return { code: 1, out: "", err: formatKeeperClientError("keeper", e) }; + } +} + +export async function keeperLoginCommand(parsed?: ParsedCli): Promise { + const opts = parsed?.opts ?? new Map(); + if (parsed && wantsCliHelp(parsed)) { + return { code: 0, out: getDetailedHelpPage("login") ?? "", err: "" }; + } + if (parsed) { + for (const secretFlag of ["password", "pass", "pwd"] as const) { + if (opts.has(secretFlag)) { + return { + code: 1, + out: "", + err: + "login: do not pass --password on the command line (it is logged and visible). " + + "Use KEEPER_PASSWORD for automation, or run `login --username …` in the shell and enter the password when prompted (masked).\n", + }; + } + } + for (const k of opts.keys()) { + if (!LOGIN_OPT_NAMES.has(k)) { + return { code: 1, out: "", err: `login: unknown option --${k}\n` }; + } + } + } + + const username = getOpt(opts, "username", "user") ?? envString("KEEPER_USERNAME"); + const passwordEnv = envString("KEEPER_PASSWORD"); + const sessionRaw = getOpt(opts, "session-token", "token", "st") ?? envString("KEEPER_SESSION_TOKEN"); + const sessionPlain = parsed && hasOpt(opts, "session-token-plain"); + + if (parsed) { + const stPlainVal = opts.get("session-token-plain"); + if (stPlainVal !== undefined && stPlainVal !== true) { + return { + code: 1, + out: "", + err: "login: --session-token-plain is a boolean flag (no value)\n", + }; + } + } + + if (!username) { + return { + code: 1, + out: "", + err: "login: provide --username or KEEPER_USERNAME.\n", + }; + } + + const sessionTrimmed = typeof sessionRaw === "string" ? sessionRaw.trim() : ""; + if (sessionTrimmed.length > 0) { + return loginWithSessionTokenCredentials(username, sessionTrimmed, { + plainToken: !!sessionPlain, + }); + } + + if (!passwordEnv) { + return { + code: 1, + needPassword: true, + loginUsername: username, + out: "", + err: "", + }; + } + + return loginWithCredentials(username, passwordEnv); +} + +export async function keeperLogoutCommand(parsed?: ParsedCli): Promise { + if (parsed && wantsCliHelp(parsed)) { + return { code: 0, out: getDetailedHelpPage("logout") ?? "", err: "" }; + } + if (parsed && parsed.opts.size > 0) { + return { code: 1, out: "", err: "logout: no options (try: logout --help)\n" }; + } + if (parsed && parsed.positional.length > 0) { + return { code: 1, out: "", err: "Usage: logout\n" }; + } + try { + const v = getVault(); + if (!v.isLoggedIn) { + return { code: 0, out: "keeper: already logged out.\n", err: "" }; + } + await v.logout(); + return { code: 0, out: "keeper: logged out.\n", err: "" }; + } catch (e) { + return { code: 1, out: "", err: formatKeeperClientError("keeper", e) }; + } +} + +export async function keeperRecordsCommand(parsed: ParsedCli): Promise { + if (wantsCliHelp(parsed)) { + return { code: 0, out: getDetailedHelpPage("records") ?? "", err: "" }; + } + if (parsed.opts.size > 0) { + return { code: 1, out: "", err: "records: unknown option (try: records --help)\n" }; + } + const sub = parsed.positional[0]?.toLowerCase(); + if (parsed.positional.length > 1) { + return { code: 1, out: "", err: "Usage: records [list]\n" }; + } + if (sub && sub !== "list") { + return { code: 1, out: "", err: "Usage: records [list]\n" }; + } + try { + const v = getVault(); + if (!v.isLoggedIn) { + const r = await keeperLoginCommand(); + if (r.code !== 0) return r; + } + await v.sync(); + const records = v.getRecords(); + const rows = records.map((r: DRecord) => `${recordUid(r)}\t${getRecordTitle(r)}`); + const header = "record_uid\ttitle\n"; + const body = rows.length ? rows.join("\n") + "\n" : "(no records)\n"; + return { code: 0, out: header + body, err: "" }; + } catch (e) { + return { code: 1, out: "", err: formatKeeperClientError("records", e) }; + } +} + +export async function keeperFoldersCommand(parsed: ParsedCli): Promise { + if (wantsCliHelp(parsed)) { + return { code: 0, out: getDetailedHelpPage("folders") ?? "", err: "" }; + } + if (parsed.opts.size > 0) { + return { code: 1, out: "", err: "folders: unknown option (try: folders --help)\n" }; + } + const sub = parsed.positional[0]?.toLowerCase(); + if (parsed.positional.length > 1) { + return { code: 1, out: "", err: "Usage: folders [list]\n" }; + } + if (sub && sub !== "list") { + return { code: 1, out: "", err: "Usage: folders [list]\n" }; + } + try { + const v = getVault(); + if (!v.isLoggedIn) { + const r = await keeperLoginCommand(); + if (r.code !== 0) return r; + } + await v.sync(); + const folders = v.getSharedFolders(); + const rows = folders.map((f: DSharedFolder) => { + const name = f.name ?? "(unnamed)"; + const uid = f.uid ?? "(unknown uid)"; + return `${uid}\t${name}`; + }); + const header = "shared_folder_uid\tname\n"; + const body = rows.length ? rows.join("\n") + "\n" : "(no shared folders)\n"; + return { code: 0, out: header + body, err: "" }; + } catch (e) { + return { code: 1, out: "", err: formatKeeperClientError("folders", e) }; + } +} + +const REGISTER_DEVICE_OPTS = new Set([ + "device-token", + "dt", + "private-key", + "pk", + "username", + "user", +]); + +export async function registerDeviceCommand(parsed: ParsedCli): Promise { + if (wantsCliHelp(parsed)) { + return { code: 0, out: getDetailedHelpPage("register-device") ?? "", err: "" }; + } + for (const k of parsed.opts.keys()) { + if (!REGISTER_DEVICE_OPTS.has(k)) { + return { code: 1, out: "", err: `register-device: unknown option --${k}\n` }; + } + } + if (parsed.positional.length > 0) { + return { + code: 1, + out: "", + err: "register-device: unexpected positional arguments\n", + }; + } + + const deviceToken = getOpt(parsed.opts, "device-token", "dt") ?? envString("REGISTER_DEVICE_TOKEN"); + const privateKey = getOpt(parsed.opts, "private-key", "pk") ?? envString("REGISTER_DEVICE_PRIVATE_KEY"); + const usernameOpt = getOpt(parsed.opts, "username", "user"); + const username = usernameOpt?.trim() || undefined; + + const dt = typeof deviceToken === "string" ? deviceToken.trim() : ""; + const pk = typeof privateKey === "string" ? privateKey.trim() : ""; + if (!dt || !pk) { + return { + code: 1, + out: "", + err: + "register-device: --device-token and --private-key required " + + "(or REGISTER_DEVICE_TOKEN / REGISTER_DEVICE_PRIVATE_KEY env).\n", + }; + } + + try { + const v = getVault(); + type RegisterFn = (dt: string, pk: string, o?: { username?: string }) => Promise; + const registerDevice = (v as { registerDevice?: RegisterFn }).registerDevice; + if (typeof registerDevice !== "function") { + return { + code: 1, + out: "", + err: + "register-device: not available in this SDK version. Upgrade @keeper-security/keeper-sdk-javascript.\n", + }; + } + await registerDevice.call(v, dt, pk, username ? { username } : undefined); + const umsg = username ? ` Last username set to ${username}.` : ""; + return { + code: 0, + out: `keeper: device credentials stored in this shell’s session.${umsg} Next: login --username … --session-token …\n`, + err: "", + }; + } catch (e) { + return { code: 1, out: "", err: formatKeeperClientError("register-device", e) }; + } +} diff --git a/shellcomponent/src/cli/mkdir.ts b/shellcomponent/src/cli/mkdir.ts new file mode 100644 index 00000000..b9b326e5 --- /dev/null +++ b/shellcomponent/src/cli/mkdir.ts @@ -0,0 +1,25 @@ +import { getDetailedHelpPage } from "./cliCommandDocs.js"; +import { wantsCliHelp, type ParsedCli } from "./cliParse.js"; +import type { CliResult } from "./types.js"; + +const MKDIR_OPTS = new Set(["p", "parents"]); + +export async function mkdirCommand(parsed: ParsedCli): Promise { + if (wantsCliHelp(parsed)) { + const doc = getDetailedHelpPage("mkdir"); + return { code: 0, out: doc ?? "", err: "" }; + } + for (const k of parsed.opts.keys()) { + if (!MKDIR_OPTS.has(k)) { + return { code: 1, out: "", err: `mkdir: unknown option --${k} (try: mkdir --help)\n` }; + } + } + + return { + code: 1, + out: "", + err: + "mkdir: not available in keeper-shell (no host filesystem). " + + "Set api-base to an HTTP CLI backend for workspace mkdir, or use Keeper vault folder APIs.\n", + }; +} diff --git a/shellcomponent/src/cli/types.ts b/shellcomponent/src/cli/types.ts new file mode 100644 index 00000000..c10abf09 --- /dev/null +++ b/shellcomponent/src/cli/types.ts @@ -0,0 +1,12 @@ +export type CliResult = { + code: number; + out: string; + err: string; + /** + * Login has username but needs password: UI should call login transport with JSON + * `{ username, password }` — never put the password in `line`. + */ + needPassword?: boolean; + /** When `needPassword`, the username from `login --username …`. */ + loginUsername?: string; +}; diff --git a/shellcomponent/src/dev-bootstrap.ts b/shellcomponent/src/dev-bootstrap.ts new file mode 100644 index 00000000..687219aa --- /dev/null +++ b/shellcomponent/src/dev-bootstrap.ts @@ -0,0 +1,42 @@ +/** + * Dev page entry only. Dynamic-imports the shell so load failures surface in the page + * (static `import "./KeeperShell.js"` would fail the whole module with no UI feedback). + */ +import "./bufferPolyfill.js"; + +function showShellLoadError(err: unknown): void { + const msg = err instanceof Error ? `${err.message}\n\n${err.stack ?? ""}` : String(err); + const hint = + "Fix:\n" + + " 1. cd shellcomponent && npm install && npm run dev\n" + + " 2. Open the http://localhost URL Vite prints (do not use file://).\n" + + " 3. With file:../KeeperSdk, the dev server uses ../KeeperSdk/src (ESM); run build:local-keeper-sdk only if you need dist/ for other tools.\n"; + document.querySelectorAll("web-console, keeper-shell").forEach((node) => { + const el = node as HTMLElement; + while (el.firstChild) el.removeChild(el.firstChild); + const pre = document.createElement("pre"); + pre.style.cssText = + "display:block;margin:0;padding:16px;background:#2a0a0a;color:#fecaca;border:2px solid #991b1b;border-radius:8px;white-space:pre-wrap;font:13px/1.45 ui-monospace,monospace;"; + pre.textContent = `Keeper shell failed to load:\n\n${msg}\n\n${hint}`; + el.appendChild(pre); + el.style.display = "block"; + el.style.width = "100%"; + el.style.minHeight = "16rem"; + el.style.boxSizing = "border-box"; + }); +} + +void (async () => { + try { + const { installKeeperDevFetchLogger } = await import("./devApiLogger.js"); + installKeeperDevFetchLogger(); + } catch (e) { + console.warn("[keeper-shell dev] fetch logger skipped (non-fatal)", e); + } + try { + await import("./KeeperShell.js"); + } catch (err: unknown) { + showShellLoadError(err); + console.error("[keeper-shell] failed to load", err); + } +})(); diff --git a/shellcomponent/src/devApiLogger.ts b/shellcomponent/src/devApiLogger.ts new file mode 100644 index 00000000..1476f6bd --- /dev/null +++ b/shellcomponent/src/devApiLogger.ts @@ -0,0 +1,137 @@ +/** + * Dev-only: wrap `fetch` to log Keeper / keeperapi HTTP calls (URL, method, headers, body summary). + * Loaded from dev-bootstrap only. {@link installKeeperDevFetchLogger} returns immediately unless + * `import.meta.env.DEV === true` (e.g. Vite dev server). + * + * Note: When a request fails, DevTools often shows `at … devApiLogger.ts` — that is this wrapper + * delegating to the real `fetch`. The failure is still from the Keeper API call (e.g. CORS from + * localhost), not a bug in this logger. + */ +const REDACT_KEYS = new Set(["authorization", "cookie", "set-cookie"]); + +function sanitizeHeaders(entries: Iterable<[string, string]>): Record { + const o: Record = {}; + for (const [k, v] of entries) { + o[k] = REDACT_KEYS.has(k.toLowerCase()) ? "[redacted]" : v; + } + return o; +} + +function hexPreview(u8: Uint8Array, maxBytes = 64): string { + const n = Math.min(maxBytes, u8.byteLength); + let s = ""; + for (let i = 0; i < n; i++) s += u8[i]!.toString(16).padStart(2, "0"); + if (u8.byteLength > n) s += "…"; + return s || "(empty)"; +} + +function summarizeBody(body: BodyInit | null | undefined): unknown { + if (body == null) return null; + if (typeof body === "string") { + const max = 4000; + return { + kind: "string", + length: body.length, + preview: body.length > max ? `${body.slice(0, max)}…` : body, + }; + } + if (body instanceof URLSearchParams) { + return { kind: "URLSearchParams", string: body.toString() }; + } + if (body instanceof FormData) { + return { kind: "FormData", keys: [...body.keys()] }; + } + if (body instanceof Blob) { + return { kind: "Blob", size: body.size, type: body.type || "(no type)" }; + } + if (body instanceof ArrayBuffer) { + const u8 = new Uint8Array(body, 0, Math.min(64, body.byteLength)); + return { kind: "ArrayBuffer", byteLength: body.byteLength, headHex: hexPreview(u8) }; + } + if (ArrayBuffer.isView(body)) { + const u8 = new Uint8Array(body.buffer, body.byteOffset, Math.min(64, body.byteLength)); + return { + kind: "ArrayBufferView", + byteLength: body.byteLength, + headHex: hexPreview(u8), + }; + } + return { kind: Object.prototype.toString.call(body) }; +} + +let installed = false; + +export function installKeeperDevFetchLogger(): void { + if (installed) return; + if (import.meta.env?.DEV !== true) return; + installed = true; + + const orig = globalThis.fetch.bind(globalThis); + let seq = 0; + + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const id = ++seq; + let url = ""; + let method = "GET"; + let headersObj: Record = {}; + let bodySummary: unknown = null; + + if (typeof input === "string" || input instanceof URL) { + url = typeof input === "string" ? input : input.href; + method = (init?.method ?? "GET").toUpperCase(); + if (init?.headers) headersObj = sanitizeHeaders(new Headers(init.headers as HeadersInit).entries()); + bodySummary = summarizeBody(init?.body ?? undefined); + } else { + url = input.url; + method = (init?.method ?? input.method ?? "GET").toUpperCase(); + const merged = new Headers(input.headers); + if (init?.headers) { + new Headers(init.headers as HeadersInit).forEach((v, k) => merged.set(k, v)); + } + headersObj = sanitizeHeaders(merged.entries()); + bodySummary = + init?.body != null + ? summarizeBody(init.body) + : { note: "body on Request object (stream); not duplicated here" }; + } + + const label = `[keeper-shell dev] HTTP #${id} ${method} ${url}`; + console.groupCollapsed(label); + console.log("request headers", headersObj); + console.log("request body summary", bodySummary); + const t0 = performance.now(); + try { + const res = await orig(input, init); + const ms = Math.round(performance.now() - t0); + console.log("response", { status: res.status, ok: res.ok, elapsedMs: ms }); + console.groupEnd(); + return res; + } catch (err) { + const ms = Math.round(performance.now() - t0); + console.error( + "%c[keeper-shell dev] FETCH FAILED%c — root cause is the request below (often CORS / offline / wrong vault host), not this wrapper file.", + "color:#b91c1c;font-weight:bold", + "color:inherit", + { id, method, url, elapsedMs: ms } + ); + console.error("Original error (expand .cause on augmented error if present):", err); + console.groupEnd(); + + const baseMsg = err instanceof Error ? err.message : String(err); + const augmented = new TypeError( + `${baseMsg} — ${method} ${url}\n` + + "(Dev fetch logger: stack frames in devApiLogger.ts are the wrapped fetch call; check Network tab for the real HTTP/CORS outcome.)" + ); + if (err instanceof Error) { + augmented.cause = err; + } + throw augmented; + } + }; + + console.info( + "%c[keeper-shell dev]%c fetch logging enabled", + "color:#0a0;font-weight:bold", + "color:inherit" + ); +} diff --git a/shellcomponent/src/index.ts b/shellcomponent/src/index.ts new file mode 100644 index 00000000..e1c1c0df --- /dev/null +++ b/shellcomponent/src/index.ts @@ -0,0 +1,17 @@ +import "./bufferPolyfill.js"; +/** Registers `` and ``. */ +import "./KeeperShell.js"; + +export { + KeeperShell, + WebConsoleElement, + WebConsoleElement as WebConsole, + KEEPER_SHELL_TAG, + WEB_CONSOLE_TAG, +} from "./KeeperShell.js"; +export { dispatchCliLine } from "./cli/cliDispatch.js"; +export { completeCliLine } from "./cli/cliComplete.js"; +export type { CliResult } from "./cli/types.js"; +export type { ShellCliContext } from "./cli/cliContext.js"; +export { setShellCliContext } from "./cli/cliContext.js"; +export { resetShellVault, loginWithCredentials } from "./cli/keeperCommands.js"; diff --git a/shellcomponent/src/shims/fs-empty.ts b/shellcomponent/src/shims/fs-empty.ts new file mode 100644 index 00000000..ed2ab6d0 --- /dev/null +++ b/shellcomponent/src/shims/fs-empty.ts @@ -0,0 +1,13 @@ +/** Browser stub: KeeperSdk SessionManager pulls `fs` for FileConfigLoader; embedded shell uses in-memory config only. */ +const notAvailable = (): never => { + throw new Error("Node fs is not available in keeper-shell (use in-memory session storage)."); +}; + +export default { + promises: { + readFile: notAvailable, + writeFile: notAvailable, + mkdir: notAvailable, + access: notAvailable, + }, +}; diff --git a/shellcomponent/src/shims/fs-promises-empty.ts b/shellcomponent/src/shims/fs-promises-empty.ts new file mode 100644 index 00000000..4e750185 --- /dev/null +++ b/shellcomponent/src/shims/fs-promises-empty.ts @@ -0,0 +1,21 @@ +/** + * Browser stub for `import fs from "fs/promises"` (KeeperSdk SessionManager / FileConfigLoader). + * The embedded shell uses {@link InMemoryConfigLoader} only; these paths are not used at runtime. + */ +const notAvailable = (): never => { + throw new Error("fs/promises is not available in keeper-shell."); +}; + +const stub = { + readFile: notAvailable, + writeFile: notAvailable, + mkdir: notAvailable, + access: notAvailable, +}; + +export default stub; + +export const readFile = notAvailable; +export const writeFile = notAvailable; +export const mkdir = notAvailable; +export const access = notAvailable; diff --git a/shellcomponent/src/shims/os-homedir.ts b/shellcomponent/src/shims/os-homedir.ts new file mode 100644 index 00000000..38f82184 --- /dev/null +++ b/shellcomponent/src/shims/os-homedir.ts @@ -0,0 +1,6 @@ +/** Minimal `os` stub for bundles that never instantiate FileConfigLoader with default paths. */ +export function homedir(): string { + return "/"; +} + +export default { homedir }; diff --git a/shellcomponent/src/vite-env.d.ts b/shellcomponent/src/vite-env.d.ts new file mode 100644 index 00000000..45ced1bb --- /dev/null +++ b/shellcomponent/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** Must match the username you use with `login` (device lookup is keyed by last_login/user). */ + readonly VITE_KEEPER_DEV_DEVICE_USER?: string; + /** Base64url device token from `.keeper/config.json` (same as `device_token`). */ + readonly VITE_KEEPER_DEV_DEVICE_TOKEN?: string; + /** Base64url EC private key from `.keeper/config.json` (same as `private_key`). */ + readonly VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY?: string; +} diff --git a/shellcomponent/tsconfig.json b/shellcomponent/tsconfig.json new file mode 100644 index 00000000..d071cfd5 --- /dev/null +++ b/shellcomponent/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["DOM", "ESNext"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@keeper-security/keeper-sdk-javascript": ["../KeeperSdk/src/index.ts"] + } + }, + "include": ["src/**/*.ts", "vite.config.ts"] +} diff --git a/shellcomponent/vite.config.ts b/shellcomponent/vite.config.ts new file mode 100644 index 00000000..2b7ac858 --- /dev/null +++ b/shellcomponent/vite.config.ts @@ -0,0 +1,63 @@ +import { defineConfig } from "vite"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +export default defineConfig({ + root: ".", + resolve: { + alias: { + "@keeper-security/keeper-sdk-javascript": resolve(__dirname, "../KeeperSdk/src/index.ts"), + "fs/promises": resolve(__dirname, "src/shims/fs-promises-empty.ts"), + fs: resolve(__dirname, "src/shims/fs-empty.ts"), + path: resolve(__dirname, "node_modules/path-browserify/index.js"), + os: resolve(__dirname, "src/shims/os-homedir.ts"), + buffer: "buffer/", + }, + }, + define: { + global: "globalThis", + }, + optimizeDeps: { + include: [ + "@keeper-security/keeper-sdk-javascript", + "@keeper-security/keeperapi", + "@xterm/xterm", + "@xterm/addon-fit", + "buffer", + "protobufjs", + ], + esbuildOptions: { + define: { + global: "globalThis", + }, + }, + }, + build: { + target: "es2017", + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "KeeperShell", + formats: ["es", "umd"], + fileName: (format) => + format === "umd" ? "keeper-shell.umd.cjs" : "keeper-shell.es.js", + }, + rollupOptions: { + output: { + exports: "named", + assetFileNames: "keeper-shell.[ext]", + }, + }, + sourcemap: true, + }, + server: { + port: 5175, + fs: { + allow: [__dirname, resolve(__dirname, "..")], + }, + }, +}); From ed283ebffb8c60fbb268fb0e8807e905425bbcd3 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Tue, 19 May 2026 09:36:27 +0530 Subject: [PATCH 03/23] removed dev logging --- shellcomponent/README.md | 2 +- shellcomponent/src/dev-bootstrap.ts | 6 -- shellcomponent/src/devApiLogger.ts | 137 ---------------------------- 3 files changed, 1 insertion(+), 144 deletions(-) delete mode 100644 shellcomponent/src/devApiLogger.ts diff --git a/shellcomponent/README.md b/shellcomponent/README.md index acd7f16b..c174bb3f 100644 --- a/shellcomponent/README.md +++ b/shellcomponent/README.md @@ -98,7 +98,7 @@ npm install npm run dev ``` -Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell and optional dev-only fetch logging. +Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell. ```bash npm run build diff --git a/shellcomponent/src/dev-bootstrap.ts b/shellcomponent/src/dev-bootstrap.ts index 687219aa..f0952882 100644 --- a/shellcomponent/src/dev-bootstrap.ts +++ b/shellcomponent/src/dev-bootstrap.ts @@ -27,12 +27,6 @@ function showShellLoadError(err: unknown): void { } void (async () => { - try { - const { installKeeperDevFetchLogger } = await import("./devApiLogger.js"); - installKeeperDevFetchLogger(); - } catch (e) { - console.warn("[keeper-shell dev] fetch logger skipped (non-fatal)", e); - } try { await import("./KeeperShell.js"); } catch (err: unknown) { diff --git a/shellcomponent/src/devApiLogger.ts b/shellcomponent/src/devApiLogger.ts deleted file mode 100644 index 1476f6bd..00000000 --- a/shellcomponent/src/devApiLogger.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Dev-only: wrap `fetch` to log Keeper / keeperapi HTTP calls (URL, method, headers, body summary). - * Loaded from dev-bootstrap only. {@link installKeeperDevFetchLogger} returns immediately unless - * `import.meta.env.DEV === true` (e.g. Vite dev server). - * - * Note: When a request fails, DevTools often shows `at … devApiLogger.ts` — that is this wrapper - * delegating to the real `fetch`. The failure is still from the Keeper API call (e.g. CORS from - * localhost), not a bug in this logger. - */ -const REDACT_KEYS = new Set(["authorization", "cookie", "set-cookie"]); - -function sanitizeHeaders(entries: Iterable<[string, string]>): Record { - const o: Record = {}; - for (const [k, v] of entries) { - o[k] = REDACT_KEYS.has(k.toLowerCase()) ? "[redacted]" : v; - } - return o; -} - -function hexPreview(u8: Uint8Array, maxBytes = 64): string { - const n = Math.min(maxBytes, u8.byteLength); - let s = ""; - for (let i = 0; i < n; i++) s += u8[i]!.toString(16).padStart(2, "0"); - if (u8.byteLength > n) s += "…"; - return s || "(empty)"; -} - -function summarizeBody(body: BodyInit | null | undefined): unknown { - if (body == null) return null; - if (typeof body === "string") { - const max = 4000; - return { - kind: "string", - length: body.length, - preview: body.length > max ? `${body.slice(0, max)}…` : body, - }; - } - if (body instanceof URLSearchParams) { - return { kind: "URLSearchParams", string: body.toString() }; - } - if (body instanceof FormData) { - return { kind: "FormData", keys: [...body.keys()] }; - } - if (body instanceof Blob) { - return { kind: "Blob", size: body.size, type: body.type || "(no type)" }; - } - if (body instanceof ArrayBuffer) { - const u8 = new Uint8Array(body, 0, Math.min(64, body.byteLength)); - return { kind: "ArrayBuffer", byteLength: body.byteLength, headHex: hexPreview(u8) }; - } - if (ArrayBuffer.isView(body)) { - const u8 = new Uint8Array(body.buffer, body.byteOffset, Math.min(64, body.byteLength)); - return { - kind: "ArrayBufferView", - byteLength: body.byteLength, - headHex: hexPreview(u8), - }; - } - return { kind: Object.prototype.toString.call(body) }; -} - -let installed = false; - -export function installKeeperDevFetchLogger(): void { - if (installed) return; - if (import.meta.env?.DEV !== true) return; - installed = true; - - const orig = globalThis.fetch.bind(globalThis); - let seq = 0; - - globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { - const id = ++seq; - let url = ""; - let method = "GET"; - let headersObj: Record = {}; - let bodySummary: unknown = null; - - if (typeof input === "string" || input instanceof URL) { - url = typeof input === "string" ? input : input.href; - method = (init?.method ?? "GET").toUpperCase(); - if (init?.headers) headersObj = sanitizeHeaders(new Headers(init.headers as HeadersInit).entries()); - bodySummary = summarizeBody(init?.body ?? undefined); - } else { - url = input.url; - method = (init?.method ?? input.method ?? "GET").toUpperCase(); - const merged = new Headers(input.headers); - if (init?.headers) { - new Headers(init.headers as HeadersInit).forEach((v, k) => merged.set(k, v)); - } - headersObj = sanitizeHeaders(merged.entries()); - bodySummary = - init?.body != null - ? summarizeBody(init.body) - : { note: "body on Request object (stream); not duplicated here" }; - } - - const label = `[keeper-shell dev] HTTP #${id} ${method} ${url}`; - console.groupCollapsed(label); - console.log("request headers", headersObj); - console.log("request body summary", bodySummary); - const t0 = performance.now(); - try { - const res = await orig(input, init); - const ms = Math.round(performance.now() - t0); - console.log("response", { status: res.status, ok: res.ok, elapsedMs: ms }); - console.groupEnd(); - return res; - } catch (err) { - const ms = Math.round(performance.now() - t0); - console.error( - "%c[keeper-shell dev] FETCH FAILED%c — root cause is the request below (often CORS / offline / wrong vault host), not this wrapper file.", - "color:#b91c1c;font-weight:bold", - "color:inherit", - { id, method, url, elapsedMs: ms } - ); - console.error("Original error (expand .cause on augmented error if present):", err); - console.groupEnd(); - - const baseMsg = err instanceof Error ? err.message : String(err); - const augmented = new TypeError( - `${baseMsg} — ${method} ${url}\n` + - "(Dev fetch logger: stack frames in devApiLogger.ts are the wrapped fetch call; check Network tab for the real HTTP/CORS outcome.)" - ); - if (err instanceof Error) { - augmented.cause = err; - } - throw augmented; - } - }; - - console.info( - "%c[keeper-shell dev]%c fetch logging enabled", - "color:#0a0;font-weight:bold", - "color:inherit" - ); -} From 740a594566e691bcd2b4818db391873847244877 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Tue, 19 May 2026 12:01:47 +0530 Subject: [PATCH 04/23] added command parsing in sdk as initial commit --- KeeperSdk/package-lock.json | 6 + KeeperSdk/package.json | 2 + KeeperSdk/src/api.ts | 224 ++++++++++++ KeeperSdk/src/auth/ConsoleAuthUI.ts | 34 +- KeeperSdk/src/auth/ConsoleLogin.ts | 19 +- KeeperSdk/src/auth/SessionManager.ts | 99 ++--- KeeperSdk/src/auth/UnavailableAuthUI.ts | 23 ++ KeeperSdk/src/auth/config.ts | 42 +++ KeeperSdk/src/auth/node/FileConfigLoader.ts | 36 ++ KeeperSdk/src/browser.ts | 6 + KeeperSdk/src/cli/commands/folders.ts | 57 +++ KeeperSdk/src/cli/commands/help.ts | 54 +++ KeeperSdk/src/cli/commands/login.ts | 179 +++++++++ KeeperSdk/src/cli/commands/logout.ts | 40 ++ KeeperSdk/src/cli/commands/records.ts | 55 +++ KeeperSdk/src/cli/commands/registerDevice.ts | 92 +++++ KeeperSdk/src/cli/dispatch.ts | 33 ++ KeeperSdk/src/cli/help.ts | 80 ++++ KeeperSdk/src/cli/index.ts | 86 +++++ KeeperSdk/src/cli/parse.ts | 173 +++++++++ KeeperSdk/src/cli/registry.ts | 55 +++ KeeperSdk/src/cli/types.ts | 62 ++++ KeeperSdk/src/cli/utils.ts | 18 + KeeperSdk/src/cli/vaultSurface.ts | 18 + KeeperSdk/src/index.ts | 17 +- KeeperSdk/src/platform/browser/platform.ts | 70 ++++ KeeperSdk/src/platform/index.ts | 20 + KeeperSdk/src/platform/node/platform.ts | 42 +++ KeeperSdk/src/platform/types.ts | 25 ++ KeeperSdk/src/records/Totp.ts | 7 +- KeeperSdk/src/vault/KeeperVault.ts | 4 +- shellcomponent/package-lock.json | 240 ++++++------ shellcomponent/src/cli/cliCommandDocs.ts | 224 ------------ shellcomponent/src/cli/cliComplete.ts | 53 +-- shellcomponent/src/cli/cliDispatch.ts | 35 +- shellcomponent/src/cli/cliHelp.ts | 99 ----- shellcomponent/src/cli/cliParse.ts | 174 --------- shellcomponent/src/cli/keeperCliHost.ts | 98 +++++ shellcomponent/src/cli/keeperCommands.ts | 365 +------------------ shellcomponent/src/cli/mkdir.ts | 25 -- shellcomponent/src/cli/mkdirCommand.ts | 40 ++ shellcomponent/src/cli/types.ts | 13 +- shellcomponent/tsconfig.json | 2 +- shellcomponent/vite.config.ts | 2 +- 44 files changed, 1845 insertions(+), 1203 deletions(-) create mode 100644 KeeperSdk/src/api.ts create mode 100644 KeeperSdk/src/auth/UnavailableAuthUI.ts create mode 100644 KeeperSdk/src/auth/config.ts create mode 100644 KeeperSdk/src/auth/node/FileConfigLoader.ts create mode 100644 KeeperSdk/src/browser.ts create mode 100644 KeeperSdk/src/cli/commands/folders.ts create mode 100644 KeeperSdk/src/cli/commands/help.ts create mode 100644 KeeperSdk/src/cli/commands/login.ts create mode 100644 KeeperSdk/src/cli/commands/logout.ts create mode 100644 KeeperSdk/src/cli/commands/records.ts create mode 100644 KeeperSdk/src/cli/commands/registerDevice.ts create mode 100644 KeeperSdk/src/cli/dispatch.ts create mode 100644 KeeperSdk/src/cli/help.ts create mode 100644 KeeperSdk/src/cli/index.ts create mode 100644 KeeperSdk/src/cli/parse.ts create mode 100644 KeeperSdk/src/cli/registry.ts create mode 100644 KeeperSdk/src/cli/types.ts create mode 100644 KeeperSdk/src/cli/utils.ts create mode 100644 KeeperSdk/src/cli/vaultSurface.ts create mode 100644 KeeperSdk/src/platform/browser/platform.ts create mode 100644 KeeperSdk/src/platform/index.ts create mode 100644 KeeperSdk/src/platform/node/platform.ts create mode 100644 KeeperSdk/src/platform/types.ts delete mode 100644 shellcomponent/src/cli/cliCommandDocs.ts delete mode 100644 shellcomponent/src/cli/cliHelp.ts delete mode 100644 shellcomponent/src/cli/cliParse.ts create mode 100644 shellcomponent/src/cli/keeperCliHost.ts delete mode 100644 shellcomponent/src/cli/mkdir.ts create mode 100644 shellcomponent/src/cli/mkdirCommand.ts diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 0d5ab53d..90fbb215 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -100,6 +100,12 @@ "undici-types": "~7.21.0" } }, + "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/prettier": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", diff --git a/KeeperSdk/package.json b/KeeperSdk/package.json index 48fc92e6..890857ee 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -9,6 +9,7 @@ }, "license": "ISC", "main": "dist/index.js", + "browser": "dist/browser.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", @@ -24,6 +25,7 @@ "dependencies": { "@keeper-security/keeperapi": "17.2.6", "ts-node": "^10.7.0", + "asmcrypto.js": "^2.3.2", "typescript": "^4.6.3" }, "devDependencies": { diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts new file mode 100644 index 00000000..ebe75903 --- /dev/null +++ b/KeeperSdk/src/api.ts @@ -0,0 +1,224 @@ +export { SessionManager } from './auth/SessionManager' +export type { + KeeperJsonConfig, + ConfigLoader, + ConfigurationUser, + ConfigurationServerConfig, + ConfigurationDeviceConfig, +} from './auth/SessionManager' +export { connectSdkPlatform, getSdkPlatform, isSdkPlatformConnected } from './platform' +export type { SdkPlatform, SdkReadline, SdkRuntime } from './platform' + +export { InMemoryStorage } from './storage/InMemoryStorage' + +export { + Logger, + ConsoleLogger, + LogLevel, + logger, + setLogger, + getLogger, + resetLogger, + KeeperSdkError, + isKeeperError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + AuthDefaults, + ResultCodes, + KEEPER_PUBLIC_HOSTS, + isBoolean, + isString, + isNonEmptyString, + isNumber, + isObject, + anyIsBoolean, + EMAIL_PATTERN, + EMAIL_LIST_SEPARATOR_PATTERN, + isValidEmail, +} from './utils' +export type { ILogger, Nullable, Optional, DeepPartial, Immutable } from './utils' + +export { + searchRecords, + formatRecord, + getRecordTitle, + getRecordType, + getRecordFields, + getRecordSummary, + getRecordPassword, + getRecordLogin, + getRecordUrl, + getRecordTotpUrl, + RecordVersion, +} from './records/RecordUtils' +export type { RecordSummary } from './records/RecordUtils' +export { parseTotpUrl, getTotpCode } from './records/Totp' +export type { TotpAlgorithm, TotpParams, TotpCode } from './records/Totp' +export { addRecord, updateRecord, deleteRecord, getRecordHistory, moveRecord } from './records/RecordOperations' +export type { + PasswordRecordData, + TypedRecordData, + RecordFieldInput, + NewRecordInput, + AddRecordResult, + UpdateRecordResult, + DeleteRecordResult, + HistoryEntry, + RecordHistoryResult, + MoveRecordInput, + MoveRecordResult, +} from './records/RecordOperations' + +export { shareRecord, removeRecordShare, getRecordShareInfo } from './sharing/Sharing' +export type { + ShareRecordInput, + ShareRecordResult, + RemoveShareInput, + RemoveShareResult, + RecordShareInfo, + RecordUserPermission, + RecordSharedFolderPermission, +} from './sharing/Sharing' + +export { KeeperVault } from './vault/KeeperVault' +export type { KeeperVaultConfig, VaultSummary } from './vault/KeeperVault' + +export { getFolder, findFolder, GetFolderFormat } from './folders/getFolder' +export type { + GetFolderOptions, + GetFolderResult, + GetFolderResultFolder, + GetFolderResultSharedFolder, + GetFolderFormatInput, + FoundFolder, +} from './folders/getFolder' + +export { + FolderKind, + ParentFolderKind, + FolderObjectType, + FolderResultStatus, + DeleteResolution, + DeleteObjectType, + folderKindFromString, +} from './folders/folderHelpers' +export type { FolderKindOrLiteral } from './folders/folderHelpers' + +export { listFolder, findFolderUidByNameOrUid, listRootUserFolders } from './folders/listFolder' +export type { + ListFolderOptions, + ListFolderResult, + ListFolderFolderSimple, + ListFolderRecordSimple, + ListFolderFolderDetail, + ListFolderRecordDetail, +} from './folders/listFolder' + +export { + listSharedFolders, + formatSharedFoldersTable, + renderSharedFoldersAsciiTable, +} from './sharedFolders/listSharedFolders' +export type { + ListSharedFoldersOptions, + ListSharedFolderRow, + FormattedSharedFoldersTable, +} from './sharedFolders/listSharedFolders' + +export { shareFolder, ShareFolderAction, ShareFolderUserResultStatus } from './sharedFolders/shareFolder' +export type { + ShareFolderActionInput, + ShareFolderInput, + ShareFolderResult, + ShareFolderUserStatus, +} from './sharedFolders/shareFolder' + +export { + changeDirectory, + createVaultFolderSession, + tryResolvePath, + resolveSingleFolder, + getWorkingFolderDisplayName, + findParentFolderUid, + splitPathComponents, +} from './folders/changeDirectory' +export type { VaultFolderSession, ChangeDirectoryResult, TryResolvePathResult } from './folders/changeDirectory' + +export { addFolder, mkdir } from './folders/addFolder' +export type { AddFolderInput, AddFolderResult, MkdirOptions } from './folders/addFolder' + +export { updateFolder, renameFolder, updateSharedFolderPermissions } from './folders/updateFolder' +export type { UpdateFolderInput, UpdateFolderResult, RenameFolderResult } from './folders/updateFolder' + +export { + deleteFolder, + rmdir, + resolveRmdirPatternsToFolderUids, + buildFolderDeleteObject, +} from './folders/deleteFolder' +export type { DeleteFolderResult, RmdirOptions } from './folders/deleteFolder' + +export { + buildFolderTree, + renderFolderTreeAscii, + folderTreeAscii, + userPermissionToText, + recordPermissionToText, +} from './folders/folderTree' +export type { FolderTreeBuildOptions, FolderTreeNode, FolderTreeResult } from './folders/folderTree' + +export { FolderManager } from './folders/FolderManager' +export type { AuthProvider, SharedFolderPermissionsInput } from './folders/FolderManager' + +export { SharedFolderManager } from './sharedFolders/SharedFolderManager' + +export { + dispatchCliLine, + dispatchKeeperCli, + ensureKeeperCliRegistry, + registerCliCommand, + registerCliAlias, + getCliCommand, + listCliCommands, + listCliCommandNames, + listDocumentedCommands, + getDetailedHelpPage, + formatDetailedHelpForCommand, + tokenizeArguments, + parseCliArgs, + wantsCliHelp, + rejectUnknownOptions, + loginWithCredentials, + loginWithSessionToken, + runLoginCommand, + runLogoutCommand, + KEEPER_VAULT_SURFACE, +} from './cli' +export type { + CliResult, + ParsedCli, + CliCommandDefinition, + CliHelpDoc, + KeeperCliHost, + KeeperCliVault, +} from './cli' + +export { Auth, KeeperEnvironment, syncDown, Authentication } from '@keeper-security/keeperapi' + +export type { + DRecord, + DRecordMetadata, + DSharedFolder, + DTeam, + DUserFolder, + VaultStorage, + SyncResult, + SyncDownOptions, + ClientConfiguration, + DeviceConfig, + SessionStorage, + AuthUI3, + KeeperError, + LoginError, +} from '@keeper-security/keeperapi' diff --git a/KeeperSdk/src/auth/ConsoleAuthUI.ts b/KeeperSdk/src/auth/ConsoleAuthUI.ts index 69a53da1..e4f8cb89 100644 --- a/KeeperSdk/src/auth/ConsoleAuthUI.ts +++ b/KeeperSdk/src/auth/ConsoleAuthUI.ts @@ -1,7 +1,6 @@ -import readline from 'readline/promises' -import { setTimeout as delay } from 'timers/promises' import type { AuthUI3, DeviceApprovalChannel, TwoFactorChannelData } from '@keeper-security/keeperapi' import { Authentication } from '@keeper-security/keeperapi' +import { getSdkPlatform } from '../platform' import { logger, extractErrorMessage, KeeperSdkError, AuthDefaults, ResultCodes } from '../utils' export class ConsoleAuthUI implements AuthUI3 { @@ -54,18 +53,19 @@ export class ConsoleAuthUI implements AuthUI3 { } private static async waitWithCancel(timeoutMs: number, cancel?: Promise): Promise { + const platform = getSdkPlatform() if (!cancel) { - await delay(timeoutMs) + await platform.delay(timeoutMs) return } - await Promise.race([delay(timeoutMs), cancel]) + await Promise.race([platform.delay(timeoutMs), cancel]) } - public async waitForDeviceApproval(channels: DeviceApprovalChannel[], isCloud: boolean): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) + public async waitForDeviceApproval(channels: DeviceApprovalChannel[], _isCloud: boolean): Promise { + const rl = getSdkPlatform().createReadline( + typeof process !== 'undefined' ? process.stdin : undefined, + typeof process !== 'undefined' ? process.stdout : undefined + ) try { logger.info('\n--- Device Approval Required ---') @@ -106,10 +106,10 @@ export class ConsoleAuthUI implements AuthUI3 { } public async waitForTwoFactorCode(channels: TwoFactorChannelData[], cancel: Promise): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) + const rl = getSdkPlatform().createReadline( + typeof process !== 'undefined' ? process.stdin : undefined, + typeof process !== 'undefined' ? process.stdout : undefined + ) try { logger.info('\n--- Two-Factor Authentication Required ---') @@ -155,10 +155,10 @@ export class ConsoleAuthUI implements AuthUI3 { } public async getPassword(isAlternate: boolean): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) + const rl = getSdkPlatform().createReadline( + typeof process !== 'undefined' ? process.stdin : undefined, + typeof process !== 'undefined' ? process.stdout : undefined + ) try { const label = isAlternate ? 'alternate master password' : 'master password' return (await rl.question(`Enter your ${label}: `)).trim() diff --git a/KeeperSdk/src/auth/ConsoleLogin.ts b/KeeperSdk/src/auth/ConsoleLogin.ts index 3d0907e3..93f30bba 100644 --- a/KeeperSdk/src/auth/ConsoleLogin.ts +++ b/KeeperSdk/src/auth/ConsoleLogin.ts @@ -1,6 +1,7 @@ -import readline from 'readline/promises' import type { AuthUI3 } from '@keeper-security/keeperapi' import { KeeperVault } from '../vault/KeeperVault' +import { getSdkPlatform } from '../platform' +import type { SdkReadline } from '../platform' import { logger, extractResultCode, @@ -12,8 +13,7 @@ import { KEEPER_PUBLIC_HOSTS, } from '../utils' import { ConsoleAuthUI } from './ConsoleAuthUI' -import { FileConfigLoader } from './SessionManager' -import type { KeeperJsonConfig } from './SessionManager' +import type { KeeperJsonConfig } from './config' const DEFAULT_REGION = 'US' const MASK_CHAR = '*' @@ -35,8 +35,6 @@ type ConsoleHandlers = { stderrWrite: typeof process.stderr.write } -const defaultConfigLoader = new FileConfigLoader() - let rlManager: ReadlineManager | null = null let suppressionDepth = 0 let originals: ConsoleHandlers | null = null @@ -69,14 +67,11 @@ function classifyInputChar(ch: string): CliCharAction { } class ReadlineManager { - private rl: readline.Interface | null = null + private rl: SdkReadline | null = null - private getOrCreate(): readline.Interface { + private getOrCreate(): SdkReadline { if (!this.rl) { - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) + this.rl = getSdkPlatform().createReadline(process.stdin, process.stdout) } return this.rl } @@ -154,7 +149,7 @@ export function prompt(question: string, masked = false): Promise { export async function loadKeeperConfig(preloaded?: KeeperJsonConfig): Promise { if (preloaded) return preloaded - return defaultConfigLoader.load() + return getSdkPlatform().createFileConfigLoader().load() } export async function resolveServer(username?: string, preloadedConfig?: KeeperJsonConfig): Promise { diff --git a/KeeperSdk/src/auth/SessionManager.ts b/KeeperSdk/src/auth/SessionManager.ts index ec3c7a9b..0370b3ce 100644 --- a/KeeperSdk/src/auth/SessionManager.ts +++ b/KeeperSdk/src/auth/SessionManager.ts @@ -1,6 +1,3 @@ -import fs from 'fs/promises' -import path from 'path' -import os from 'os' import { normal64Bytes, type DeviceConfig, @@ -8,37 +5,24 @@ import { type KeeperHost, type SessionParams, } from '@keeper-security/keeperapi' -import { logger, extractErrorMessage, SdkDefaults } from '../utils' +import { getSdkPlatform } from '../platform' +import { logger, extractErrorMessage } from '../utils' import type { Nullable } from '../utils' - -export type ConfigurationUser = { - user?: string - server?: string - last_device?: { device_token?: string } -} - -export type ConfigurationServerConfig = { - server?: string - clone_code?: string -} - -export type ConfigurationDeviceConfig = { - device_token?: string - private_key?: string - server_info?: Array -} - -export type KeeperJsonConfig = { - last_login?: string - last_server?: string - user?: string - server?: string - device_token?: string - private_key?: string - clone_code?: string - users?: Array - devices?: Array -} +import type { + ConfigLoader, + KeeperJsonConfig, + ConfigurationServerConfig, + ConfigurationUser, +} from './config' +import { isValidKeeperConfig } from './config' + +export type { + KeeperJsonConfig, + ConfigLoader, + ConfigurationUser, + ConfigurationServerConfig, + ConfigurationDeviceConfig, +} from './config' type ResolvedDevice = { deviceToken: Uint8Array @@ -51,41 +35,6 @@ type DeviceCacheEntry = { device: Nullable } -export interface ConfigLoader { - load(): Promise - save(config: KeeperJsonConfig): Promise - readonly configDir: string -} - -export class FileConfigLoader implements ConfigLoader { - public readonly configDir: string - - constructor(configDir?: string) { - this.configDir = configDir || path.join(os.homedir(), SdkDefaults.CONFIG_DIR) - } - - async load(): Promise { - const configPath = path.join(this.configDir, 'config.json') - try { - const content = await fs.readFile(configPath, 'utf-8') - const parsed: unknown = JSON.parse(content) - if (SessionManager.isValidKeeperConfig(parsed)) { - return parsed - } - } catch (err) { - logger.debug('Failed to load keeper config:', extractErrorMessage(err)) - } - return {} - } - - async save(config: KeeperJsonConfig): Promise { - const configPath = path.join(this.configDir, 'config.json') - await fs.writeFile(configPath, JSON.stringify(config, null, 2), { - mode: 0o600, - }) - } -} - export class SessionManager implements SessionStorage { private readonly configLoader: ConfigLoader private sessionParams: Nullable = null @@ -98,8 +47,10 @@ export class SessionManager implements SessionStorage { constructor(configDir?: string) constructor(loader: ConfigLoader) constructor(configDirOrLoader?: string | ConfigLoader) { - if (typeof configDirOrLoader === 'string' || configDirOrLoader === undefined) { - this.configLoader = new FileConfigLoader(configDirOrLoader as string | undefined) + if (typeof configDirOrLoader === 'string') { + this.configLoader = getSdkPlatform().createFileConfigLoader(configDirOrLoader) + } else if (configDirOrLoader === undefined) { + this.configLoader = getSdkPlatform().createFileConfigLoader() } else { this.configLoader = configDirOrLoader } @@ -187,7 +138,7 @@ export class SessionManager implements SessionStorage { ) if (user?.last_device?.device_token) { const device = (parsed.devices || []).find( - (configDevice) => configDevice.device_token === user.last_device.device_token + (configDevice) => configDevice.device_token === user.last_device!.device_token ) if (device?.server_info) { const serverInfo = device.server_info.find((entry) => entry.server === host) @@ -284,10 +235,6 @@ export class SessionManager implements SessionStorage { } public static isValidKeeperConfig(value: unknown): value is KeeperJsonConfig { - if (typeof value !== 'object' || value === null) return false - const obj = value as Record - if (obj.users !== undefined && !Array.isArray(obj.users)) return false - if (obj.devices !== undefined && !Array.isArray(obj.devices)) return false - return true + return isValidKeeperConfig(value) } } diff --git a/KeeperSdk/src/auth/UnavailableAuthUI.ts b/KeeperSdk/src/auth/UnavailableAuthUI.ts new file mode 100644 index 00000000..16278777 --- /dev/null +++ b/KeeperSdk/src/auth/UnavailableAuthUI.ts @@ -0,0 +1,23 @@ +import type { AuthUI3, DeviceApprovalChannel, TwoFactorChannelData } from '@keeper-security/keeperapi' +import { KeeperSdkError, ResultCodes } from '../utils' + +const MSG = + 'Console authentication is disabled (useConsoleAuth: false). Provide authUI or use a host that handles login (e.g. keeper-shell password prompt).' + +/** Placeholder when Node readline-based ConsoleAuthUI must not be used. */ +export class UnavailableAuthUI implements AuthUI3 { + public async waitForDeviceApproval(_channels: DeviceApprovalChannel[], _isCloud: boolean): Promise { + throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN) + } + + public async waitForTwoFactorCode( + _channels: TwoFactorChannelData[], + _cancel: Promise + ): Promise { + throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN) + } + + public async getPassword(_isAlternate: boolean): Promise { + throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN) + } +} diff --git a/KeeperSdk/src/auth/config.ts b/KeeperSdk/src/auth/config.ts new file mode 100644 index 00000000..af3d2dd7 --- /dev/null +++ b/KeeperSdk/src/auth/config.ts @@ -0,0 +1,42 @@ +export type ConfigurationUser = { + user?: string + server?: string + last_device?: { device_token?: string } +} + +export type ConfigurationServerConfig = { + server?: string + clone_code?: string +} + +export type ConfigurationDeviceConfig = { + device_token?: string + private_key?: string + server_info?: Array +} + +export type KeeperJsonConfig = { + last_login?: string + last_server?: string + user?: string + server?: string + device_token?: string + private_key?: string + clone_code?: string + users?: Array + devices?: Array +} + +export interface ConfigLoader { + load(): Promise + save(config: KeeperJsonConfig): Promise + readonly configDir: string +} + +export function isValidKeeperConfig(value: unknown): value is KeeperJsonConfig { + if (typeof value !== 'object' || value === null) return false + const obj = value as Record + if (obj.users !== undefined && !Array.isArray(obj.users)) return false + if (obj.devices !== undefined && !Array.isArray(obj.devices)) return false + return true +} diff --git a/KeeperSdk/src/auth/node/FileConfigLoader.ts b/KeeperSdk/src/auth/node/FileConfigLoader.ts new file mode 100644 index 00000000..61c4d08e --- /dev/null +++ b/KeeperSdk/src/auth/node/FileConfigLoader.ts @@ -0,0 +1,36 @@ +import fs from 'fs/promises' +import path from 'path' +import os from 'os' +import type { ConfigLoader, KeeperJsonConfig } from '../config' +import { isValidKeeperConfig } from '../config' +import { logger, extractErrorMessage, SdkDefaults } from '../../utils' + +/** Node-only: read/write `~/.keeper/config.json`. */ +export class FileConfigLoader implements ConfigLoader { + public readonly configDir: string + + constructor(configDir?: string) { + this.configDir = configDir || path.join(os.homedir(), SdkDefaults.CONFIG_DIR) + } + + async load(): Promise { + const configPath = path.join(this.configDir, 'config.json') + try { + const content = await fs.readFile(configPath, 'utf-8') + const parsed: unknown = JSON.parse(content) + if (isValidKeeperConfig(parsed)) { + return parsed + } + } catch (err) { + logger.debug('Failed to load keeper config:', extractErrorMessage(err)) + } + return {} + } + + async save(config: KeeperJsonConfig): Promise { + const configPath = path.join(this.configDir, 'config.json') + await fs.writeFile(configPath, JSON.stringify(config, null, 2), { + mode: 0o600, + }) + } +} diff --git a/KeeperSdk/src/browser.ts b/KeeperSdk/src/browser.ts new file mode 100644 index 00000000..463daba6 --- /dev/null +++ b/KeeperSdk/src/browser.ts @@ -0,0 +1,6 @@ +import { connectSdkPlatform } from './platform' +import { browserSdkPlatform } from './platform/browser/platform' + +connectSdkPlatform(browserSdkPlatform) + +export * from './api' diff --git a/KeeperSdk/src/cli/commands/folders.ts b/KeeperSdk/src/cli/commands/folders.ts new file mode 100644 index 00000000..fe47dde4 --- /dev/null +++ b/KeeperSdk/src/cli/commands/folders.ts @@ -0,0 +1,57 @@ +import type { DSharedFolder } from '@keeper-security/keeperapi' +import type { CliCommandDefinition, KeeperCliHost } from '../types' +import { wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureLoggedIn } from './login' + +export const foldersCommand: CliCommandDefinition = { + name: 'folders', + order: 31, + description: 'List shared folders in the vault (uses env or `login --username …`).', + usage: 'folders [list] [--help|-h]', + subcommands: ['list'], + help: { + title: 'folders — list shared folders', + synopsis: ' folders [list]', + description: ' Runs sync, then prints shared_folder_uid and name for each shared folder.', + arguments: ' list Optional; default is list.', + options: ' --help, -h Show this help.', + keeperSdk: ` Uses KeeperVault.sync(), getSharedFolders(). + Related: listSharedFolders, shareFolder, FolderManager / SharedFolderManager.`, + appendVaultSurface: true, + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(foldersCommand), err: '' } + } + if (parsed.opts.size > 0) { + return { code: 1, out: '', err: 'folders: unknown option (try: folders --help)\n' } + } + const sub = parsed.positional[0]?.toLowerCase() + if (parsed.positional.length > 1) { + return { code: 1, out: '', err: 'Usage: folders [list]\n' } + } + if (sub && sub !== 'list') { + return { code: 1, out: '', err: 'Usage: folders [list]\n' } + } + try { + const v = host.getVault() + if (!v.isLoggedIn) { + const r = await ensureLoggedIn(host) + if (r.code !== 0) return r + } + await v.sync() + const folders = v.getSharedFolders() + const rows = folders.map((f: DSharedFolder) => { + const name = f.name ?? '(unnamed)' + const uid = f.uid ?? '(unknown uid)' + return `${uid}\t${name}` + }) + const header = 'shared_folder_uid\tname\n' + const body = rows.length ? rows.join('\n') + '\n' : '(no shared folders)\n' + return { code: 0, out: header + body, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError('folders', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/help.ts b/KeeperSdk/src/cli/commands/help.ts new file mode 100644 index 00000000..2ce80544 --- /dev/null +++ b/KeeperSdk/src/cli/commands/help.ts @@ -0,0 +1,54 @@ +import type { CliCommandDefinition, KeeperCliHost } from '../types' +import { wantsCliHelp } from '../parse' +import { + formatAllCommandsSummary, + formatDetailedHelpForCommand, + formatShortCommandSummary, + getDetailedHelpPageForRegistry, +} from '../help' +import { listCliCommands } from '../registry' + +export const helpCommand: CliCommandDefinition = { + name: 'help', + order: 0, + description: 'Show all commands, or full docs for one command (same as COMMAND --help).', + usage: 'help [command] (see also: help --help)', + help: { + title: 'help — show commands or short syntax for one command', + synopsis: ' help [COMMAND]', + description: ` Without arguments, lists every built-in command with a one-line summary. + With COMMAND, prints the same overview line plus usage for that command. + + For full documentation on each command, run: + COMMAND --help + COMMAND -h`, + options: ' None. This command does not take GNU-style flags.', + seeAlso: ' Each command’s --help output.', + }, + async run(_host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(helpCommand), err: '' } + } + if (parsed.opts.size > 0) { + return { code: 1, out: '', err: 'help: unknown option (try `help --help`)\n' } + } + const args = parsed.positional + if (args.length === 0) { + return { code: 0, out: formatAllCommandsSummary(listCliCommands()), err: '' } + } + if (args.length > 1) { + return { code: 1, out: '', err: 'Usage: help [command]\n' } + } + const name = args[0] + const long = getDetailedHelpPageForRegistry(listCliCommands(), name) + if (long) { + return { code: 0, out: long, err: '' } + } + const commands = listCliCommands() + const def = commands.find((c) => c.name === name.toLowerCase()) + if (!def) { + return { code: 1, out: '', err: `help: unknown command: ${name}\n` } + } + return { code: 0, out: formatShortCommandSummary(def), err: '' } + }, +} diff --git a/KeeperSdk/src/cli/commands/login.ts b/KeeperSdk/src/cli/commands/login.ts new file mode 100644 index 00000000..ab4d4540 --- /dev/null +++ b/KeeperSdk/src/cli/commands/login.ts @@ -0,0 +1,179 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { getOpt, hasOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { utf8ToBase64Url } from '../utils' + +const LOGIN_ALLOWED = new Set([ + 'username', + 'user', + 'session-token', + 'token', + 'st', + 'session-token-plain', +]) + +export async function runLoginCommand(host: KeeperCliHost, parsed?: ParsedCli): Promise { + const opts = parsed?.opts ?? new Map() + if (parsed && wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(loginCommand), err: '' } + } + if (parsed) { + for (const secretFlag of ['password', 'pass', 'pwd'] as const) { + if (opts.has(secretFlag)) { + return { + code: 1, + out: '', + err: + 'login: do not pass --password on the command line (it is logged and visible). ' + + 'Use KEEPER_PASSWORD for automation, or run `login --username …` in the shell and enter the password when prompted (masked).\n', + } + } + } + const bad = rejectUnknownOptions(parsed, LOGIN_ALLOWED, 'login') + if (bad) return bad + } + + const username = getOpt(opts, 'username', 'user') ?? host.envString('KEEPER_USERNAME') + const passwordEnv = host.envString('KEEPER_PASSWORD') + const sessionRaw = getOpt(opts, 'session-token', 'token', 'st') ?? host.envString('KEEPER_SESSION_TOKEN') + const sessionPlain = parsed && hasOpt(opts, 'session-token-plain') + + if (parsed) { + const stPlainVal = opts.get('session-token-plain') + if (stPlainVal !== undefined && stPlainVal !== true) { + return { + code: 1, + out: '', + err: 'login: --session-token-plain is a boolean flag (no value)\n', + } + } + } + + if (!username) { + return { + code: 1, + out: '', + err: 'login: provide --username or KEEPER_USERNAME.\n', + } + } + + const sessionTrimmed = typeof sessionRaw === 'string' ? sessionRaw.trim() : '' + if (sessionTrimmed.length > 0) { + return loginWithSessionToken(host, username, sessionTrimmed, { plainToken: !!sessionPlain }) + } + + if (!passwordEnv) { + return { + code: 1, + needPassword: true, + loginUsername: username, + out: '', + err: '', + } + } + + return loginWithCredentials(host, username, passwordEnv) +} + +export async function loginWithCredentials( + host: KeeperCliHost, + username: string, + password: string +): Promise { + try { + const v = host.getVault() + if (v.isLoggedIn) { + await v.logout() + } + await v.login(username, password) + await v.sync() + return { code: 0, out: `keeper: logged in as ${username}.\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError('keeper', e) } + } +} + +export async function loginWithSessionToken( + host: KeeperCliHost, + username: string, + sessionToken: string, + options?: { plainToken?: boolean } +): Promise { + let token = sessionToken.trim() + if (options?.plainToken && token.length > 0) { + token = utf8ToBase64Url(token) + } + try { + const v = host.getVault() + if (v.isLoggedIn) { + await v.logout() + } + await v.loginWithSessionToken(username, token) + await v.sync() + return { code: 0, out: `keeper: logged in as ${username} (session token).\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError('keeper', e) } + } +} + +/** Attempt env/password login when vault commands need a session. */ +export async function ensureLoggedIn(host: KeeperCliHost): Promise { + if (host.getVault().isLoggedIn) { + return { code: 0, out: '', err: '' } + } + return runLoginCommand(host, { positional: [], opts: new Map() }) +} + +export const loginCommand: CliCommandDefinition = { + name: 'login', + order: 10, + description: + 'Log in with password (env / masked prompt) or session token (flag or KEEPER_SESSION_TOKEN). Password never on CLI line.', + usage: + 'login [--username|--user ] [--session-token|--token|--st ] [--session-token-plain] [--help|-h]', + flagOptions: [ + '--user', + '--username', + '--session-token', + '--token', + '--st', + '--session-token-plain', + ], + allowedOptions: LOGIN_ALLOWED, + help: { + title: 'login — authenticate to Keeper (vault session)', + synopsis: ` login [--username|--user EMAIL_OR_NAME] + login [--username|--user U] [--session-token|--token|--st TOKEN] + login [--username|--user U] [--session-token TOKEN] [--session-token-plain]`, + description: ` Establishes a Keeper session using KeeperVault. + + Username comes from --username / --user or KEEPER_USERNAME. + + Password MUST NOT appear on the CLI line (logging, proxies, browser history). + Automation: set KEEPER_PASSWORD in the environment when embedding in Node. + Web shell: run login with only a username; the UI prompts for a masked password + and sends it through the login transport, not in "line". + + Session token login uses KeeperVault.loginWithSessionToken. The token may be + passed on the command line or via KEEPER_SESSION_TOKEN (sensitive — same + caveats as any secret on argv). + + --session-token-plain encodes the token from UTF-8 to base64url before the + SDK call (same idea as the session_token_login example when the token is raw text). + + Device registration: session token login requires deviceToken + privateKey for + this host in session storage. Use register-device (or a prior password login in + this shell) to store them; see register-device --help.`, + options: ` --username, --user Account identifier (often email). + --session-token, --token, --st Session token string (or use KEEPER_SESSION_TOKEN). + --session-token-plain Treat --session-token value as plain UTF-8 and encode base64url.`, + environment: ` KEEPER_USERNAME Default username if not passed on the command line. + KEEPER_PASSWORD Password for non-interactive login (no session token). + KEEPER_SESSION_TOKEN Session token when not passed as a flag. + KEEPER_HOST Optional vault host / region (also: keeper-host attribute).`, + keeperSdk: ` Uses KeeperVault.login or loginWithSessionToken, then sync. resumeSession and + clone-code flows exist in the SDK but are not exposed in this CLI yet.`, + appendVaultSurface: true, + }, + run: (host, parsed) => runLoginCommand(host, parsed), +} diff --git a/KeeperSdk/src/cli/commands/logout.ts b/KeeperSdk/src/cli/commands/logout.ts new file mode 100644 index 00000000..770527d7 --- /dev/null +++ b/KeeperSdk/src/cli/commands/logout.ts @@ -0,0 +1,40 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' + +export async function runLogoutCommand(host: KeeperCliHost, parsed?: ParsedCli): Promise { + if (parsed && wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(logoutCommand), err: '' } + } + if (parsed && parsed.opts.size > 0) { + return { code: 1, out: '', err: 'logout: no options (try: logout --help)\n' } + } + if (parsed && parsed.positional.length > 0) { + return { code: 1, out: '', err: 'Usage: logout\n' } + } + try { + const v = host.getVault() + if (!v.isLoggedIn) { + return { code: 0, out: 'keeper: already logged out.\n', err: '' } + } + await v.logout() + return { code: 0, out: 'keeper: logged out.\n', err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError('keeper', e) } + } +} + +export const logoutCommand: CliCommandDefinition = { + name: 'logout', + order: 200, + description: 'Log out of the current Keeper session.', + usage: 'logout [--help|-h]', + help: { + title: 'logout — end the current Keeper session', + synopsis: ' logout', + description: ' Calls KeeperVault.logout when a session exists.', + options: ' None.', + appendVaultSurface: true, + }, + run: (host, parsed) => runLogoutCommand(host, parsed), +} diff --git a/KeeperSdk/src/cli/commands/records.ts b/KeeperSdk/src/cli/commands/records.ts new file mode 100644 index 00000000..d7b5807d --- /dev/null +++ b/KeeperSdk/src/cli/commands/records.ts @@ -0,0 +1,55 @@ +import { getRecordTitle } from '../../records/RecordUtils' +import type { CliCommandDefinition, KeeperCliHost, ParsedCli } from '../types' +import { wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { recordUid } from '../utils' +import { ensureLoggedIn } from './login' + +export const recordsCommand: CliCommandDefinition = { + name: 'records', + order: 30, + description: 'List vault records (uid and title).', + usage: 'records [list] [--help|-h]', + subcommands: ['list'], + help: { + title: 'records — list vault records (record UID and title)', + synopsis: ' records [list]', + description: ' Runs sync, then prints a table of record_uid and title for each record.', + arguments: ' list Optional; default behavior is to list. Other subcommands may be added later.', + options: ' --help, -h Show this help.', + keeperSdk: ` Maps to KeeperVault.sync(), getRecords(), getRecordTitle(). + Related APIs you can extend in this CLI later: findRecords, getRecordsByType, + addRecord, updateRecord, deleteRecord, shareRecord, getRecordHistory, …`, + appendVaultSurface: true, + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(recordsCommand), err: '' } + } + if (parsed.opts.size > 0) { + return { code: 1, out: '', err: 'records: unknown option (try: records --help)\n' } + } + const sub = parsed.positional[0]?.toLowerCase() + if (parsed.positional.length > 1) { + return { code: 1, out: '', err: 'Usage: records [list]\n' } + } + if (sub && sub !== 'list') { + return { code: 1, out: '', err: 'Usage: records [list]\n' } + } + try { + const v = host.getVault() + if (!v.isLoggedIn) { + const r = await ensureLoggedIn(host) + if (r.code !== 0) return r + } + await v.sync() + const records = v.getRecords() + const rows = records.map((r) => `${recordUid(r)}\t${getRecordTitle(r)}`) + const header = 'record_uid\ttitle\n' + const body = rows.length ? rows.join('\n') + '\n' : '(no records)\n' + return { code: 0, out: header + body, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError('records', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/registerDevice.ts b/KeeperSdk/src/cli/commands/registerDevice.ts new file mode 100644 index 00000000..64596395 --- /dev/null +++ b/KeeperSdk/src/cli/commands/registerDevice.ts @@ -0,0 +1,92 @@ +import type { CliCommandDefinition, KeeperCliHost } from '../types' +import { getOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' + +const REGISTER_DEVICE_ALLOWED = new Set(['device-token', 'dt', 'private-key', 'pk', 'username', 'user']) + +export const registerDeviceCommand: CliCommandDefinition = { + name: 'register-device', + order: 15, + description: + 'Store device token + private key in this shell’s session so session-token login works (see login --help).', + usage: + 'register-device --device-token|--dt --private-key|--pk [--username ] [--help|-h]', + flagOptions: [ + '--device-token', + '--dt', + '--private-key', + '--pk', + '--user', + '--username', + ], + allowedOptions: REGISTER_DEVICE_ALLOWED, + help: { + title: 'register-device — store device token and private key for session-token login', + synopsis: + ' register-device --device-token|--dt B64 --private-key|--pk B64 [--username|--user U]', + description: ` Calls KeeperVault.registerDevice to save device credentials for the current + host in this shell’s in-memory session. After this, you can run: + + login --username YOU --session-token TOKEN + + without a prior password login in this shell, as long as the token is valid. + + Obtain device_token and private_key from another machine’s keeper config after + a successful login, or from your integration that provisions device keys. + Values accept base64 or base64url (same decoding as SessionManager / normal64Bytes).`, + options: ` --device-token, --dt Device token string. + --private-key, --pk Device private key string. + --username, --user Optional; sets last username in session storage (recommended).`, + environment: ` REGISTER_DEVICE_TOKEN Same as --device-token when flag omitted. + REGISTER_DEVICE_PRIVATE_KEY Same as --private-key when flag omitted. + KEEPER_HOST Same as other keeper commands.`, + keeperSdk: ' KeeperVault.registerDevice(deviceToken, privateKey, { username? })', + appendVaultSurface: true, + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(registerDeviceCommand), err: '' } + } + const bad = rejectUnknownOptions(parsed, REGISTER_DEVICE_ALLOWED, 'register-device') + if (bad) return bad + if (parsed.positional.length > 0) { + return { + code: 1, + out: '', + err: 'register-device: unexpected positional arguments\n', + } + } + + const deviceToken = + getOpt(parsed.opts, 'device-token', 'dt') ?? host.envString('REGISTER_DEVICE_TOKEN') + const privateKey = + getOpt(parsed.opts, 'private-key', 'pk') ?? host.envString('REGISTER_DEVICE_PRIVATE_KEY') + const usernameOpt = getOpt(parsed.opts, 'username', 'user') + const username = usernameOpt?.trim() || undefined + + const dt = typeof deviceToken === 'string' ? deviceToken.trim() : '' + const pk = typeof privateKey === 'string' ? privateKey.trim() : '' + if (!dt || !pk) { + return { + code: 1, + out: '', + err: + 'register-device: --device-token and --private-key required ' + + '(or REGISTER_DEVICE_TOKEN / REGISTER_DEVICE_PRIVATE_KEY env).\n', + } + } + + try { + const v = host.getVault() + await v.registerDevice(dt, pk, username ? { username } : undefined) + const umsg = username ? ` Last username set to ${username}.` : '' + return { + code: 0, + out: `keeper: device credentials stored in this shell’s session.${umsg} Next: login --username … --session-token …\n`, + err: '', + } + } catch (e) { + return { code: 1, out: '', err: host.formatError('register-device', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/dispatch.ts b/KeeperSdk/src/cli/dispatch.ts new file mode 100644 index 00000000..0b6e0175 --- /dev/null +++ b/KeeperSdk/src/cli/dispatch.ts @@ -0,0 +1,33 @@ +import type { CliResult, KeeperCliHost } from './types' +import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' +import { formatDetailedHelpForCommand } from './help' +import { getCliCommand } from './registry' + +export async function dispatchKeeperCli( + commandName: string, + args: string[], + host: KeeperCliHost +): Promise { + const def = getCliCommand(commandName) + if (!def) { + return { code: 1, out: '', err: `Unknown command: ${commandName}\n` } + } + const parsed = parseCliArgs(args) + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(def), err: '' } + } + return def.run(host, parsed) +} + +export async function dispatchCliLine(line: string, host: KeeperCliHost): Promise { + const trimmed = line.trim() + if (!trimmed) { + return { code: 0, out: '', err: '' } + } + const tokens = tokenizeArguments(trimmed) + const name = tokens[0]?.toLowerCase() + if (!name) { + return { code: 0, out: '', err: '' } + } + return dispatchKeeperCli(name, tokens.slice(1), host) +} diff --git a/KeeperSdk/src/cli/help.ts b/KeeperSdk/src/cli/help.ts new file mode 100644 index 00000000..b52611d3 --- /dev/null +++ b/KeeperSdk/src/cli/help.ts @@ -0,0 +1,80 @@ +import type { CliCommandDefinition, CliHelpDoc } from './types' +import { KEEPER_VAULT_SURFACE } from './vaultSurface' + +const SECTION_ORDER: (keyof CliHelpDoc)[] = [ + 'synopsis', + 'description', + 'arguments', + 'options', + 'environment', + 'keeperSdk', + 'seeAlso', + 'note', +] + +const SECTION_LABELS: Partial> = { + synopsis: 'SYNOPSIS', + description: 'DESCRIPTION', + arguments: 'ARGUMENTS', + options: 'OPTIONS', + environment: 'ENVIRONMENT', + keeperSdk: 'KEEPER SDK', + seeAlso: 'SEE ALSO', + note: 'NOTE', +} + +export function formatDetailedHelp(doc: CliHelpDoc): string { + const parts: string[] = [doc.title.trim()] + for (const key of SECTION_ORDER) { + const body = doc[key] + if (typeof body !== 'string' || !body.trim()) continue + const label = SECTION_LABELS[key] + if (label) { + parts.push('') + parts.push(label) + } + parts.push(body.trim()) + } + if (doc.appendVaultSurface) { + parts.push('') + parts.push(KEEPER_VAULT_SURFACE) + } + return `${parts.join('\n')}\n` +} + +export function formatDetailedHelpForCommand(def: CliCommandDefinition): string { + return formatDetailedHelp(def.help) +} + +export function getDetailedHelpPageForRegistry( + commands: Iterable, + name: string +): string | null { + const key = name.toLowerCase() + for (const def of commands) { + if (def.name === key) return formatDetailedHelpForCommand(def) + if (def.aliases?.some((a) => a.toLowerCase() === key)) { + return formatDetailedHelpForCommand(def) + } + } + return null +} + +export function formatAllCommandsSummary(commands: readonly CliCommandDefinition[]): string { + const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name)) + const w = Math.max(...sorted.map((c) => c.name.length), 8) + let out = 'Supported commands:\n\n' + for (const c of sorted) { + out += ` ${c.name.padEnd(w)} ${c.description}\n` + } + out += + '\nOptions use GNU-style syntax: `--name`, `--name=value`, short flags (`-p` or `-rf` when each letter is a switch), and `--` ends options.\n' + out += 'Quoted arguments and `\\` escapes are supported.\n\n' + out += + 'Run `help ` for a short summary, or `command --help` / `command -h` for full documentation.\n' + return out +} + +export function formatShortCommandSummary(def: CliCommandDefinition): string { + return `${def.name} — ${def.description}\n Usage: ${def.usage}\n` +} diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts new file mode 100644 index 00000000..a6bd67bc --- /dev/null +++ b/KeeperSdk/src/cli/index.ts @@ -0,0 +1,86 @@ +import { registerCliCommand } from './registry' +import { foldersCommand } from './commands/folders' +import { helpCommand } from './commands/help' +import { loginCommand } from './commands/login' +import { logoutCommand } from './commands/logout' +import { recordsCommand } from './commands/records' +import { registerDeviceCommand } from './commands/registerDevice' + +let registryInitialized = false + +/** Register built-in Keeper CLI commands (idempotent). */ +export function ensureKeeperCliRegistry(): void { + if (registryInitialized) return + registryInitialized = true + registerCliCommand(helpCommand) + registerCliCommand(loginCommand) + registerCliCommand(registerDeviceCommand) + registerCliCommand(recordsCommand) + registerCliCommand(foldersCommand) + registerCliCommand(logoutCommand) +} + +ensureKeeperCliRegistry() + +export type { + CliResult, + ParsedCli, + CliHelpDoc, + CliCommandDefinition, + KeeperCliHost, + KeeperCliVault, +} from './types' + +export { + tokenizeArguments, + parseCliArgs, + hasOpt, + getOpt, + wantsCliHelp, + rejectUnknownOptions, +} from './parse' + +export { + formatDetailedHelp, + formatDetailedHelpForCommand, + formatAllCommandsSummary, + formatShortCommandSummary, +} from './help' +import { getDetailedHelpPageForRegistry } from './help' +import { listCliCommands } from './registry' + +export function getDetailedHelpPage(name: string): string | null { + ensureKeeperCliRegistry() + return getDetailedHelpPageForRegistry(listCliCommands(), name) +} + +export { KEEPER_VAULT_SURFACE } from './vaultSurface' + +export { + registerCliCommand, + registerCliAlias, + resolveCliCommandName, + getCliCommand, + listCliCommands, + listCliCommandNames, + listDocumentedCommands, + clearCliRegistry, +} from './registry' + +export { dispatchKeeperCli, dispatchCliLine } from './dispatch' + +export { + runLoginCommand, + loginWithCredentials, + loginWithSessionToken, + ensureLoggedIn, + loginCommand, +} from './commands/login' + +export { runLogoutCommand, logoutCommand } from './commands/logout' +export { recordsCommand } from './commands/records' +export { foldersCommand } from './commands/folders' +export { registerDeviceCommand } from './commands/registerDevice' +export { helpCommand } from './commands/help' + +export { utf8ToBase64Url, recordUid } from './utils' diff --git a/KeeperSdk/src/cli/parse.ts b/KeeperSdk/src/cli/parse.ts new file mode 100644 index 00000000..73f9ef15 --- /dev/null +++ b/KeeperSdk/src/cli/parse.ts @@ -0,0 +1,173 @@ +import type { CliResult, ParsedCli } from './types' + +const isWhitespace = (ch: string) => /\s/.test(ch) + +/** Split a command line into tokens; respects double quotes and `\\` escapes. */ +export function tokenizeArguments(args: string): string[] { + const out: string[] = [] + const sb: string[] = [] + let pos = 0 + let inQuote = false + let escape = false + + const flush = () => { + if (sb.length > 0) { + out.push(sb.join('')) + sb.length = 0 + } + } + + while (pos < args.length) { + const ch = args[pos] + if (escape) { + escape = false + sb.push(ch) + pos++ + continue + } + if (inQuote) { + if (ch === '\\') { + escape = true + pos++ + continue + } + if (ch === '"') { + inQuote = false + pos++ + continue + } + sb.push(ch) + pos++ + continue + } + switch (ch) { + case '\\': + escape = true + pos++ + break + case '"': + inQuote = true + pos++ + break + default: + if (isWhitespace(ch)) { + flush() + pos++ + } else { + sb.push(ch) + pos++ + } + } + } + flush() + return out +} + +function setBool(opts: Map, k: string): void { + opts.set(k.toLowerCase(), true) +} + +function setStr(opts: Map, k: string, v: string): void { + opts.set(k.toLowerCase(), v) +} + +/** Parse argv-style tokens after the command name. */ +export function parseCliArgs(tokens: string[]): ParsedCli { + const positional: string[] = [] + const opts = new Map() + + let i = 0 + while (i < tokens.length) { + const t = tokens[i] + if (t === '--') { + positional.push(...tokens.slice(i + 1)) + break + } + if (t === '-' || !t.startsWith('-')) { + positional.push(t) + i++ + continue + } + + if (t.startsWith('--')) { + const body = t.slice(2) + if (!body) { + positional.push(t) + i++ + continue + } + const eq = body.indexOf('=') + if (eq >= 0) { + setStr(opts, body.slice(0, eq), body.slice(eq + 1)) + i++ + continue + } + const name = body + const next = tokens[i + 1] + if (next && next !== '--' && !next.startsWith('-')) { + setStr(opts, name, next) + i += 2 + continue + } + setBool(opts, name) + i++ + continue + } + + const rest = t.slice(1) + if (!rest) { + positional.push(t) + i++ + continue + } + if (/^[A-Za-z]$/.test(rest)) { + setBool(opts, rest) + i++ + continue + } + if (/^[A-Za-z]+$/.test(rest)) { + for (const ch of rest) setBool(opts, ch) + i++ + continue + } + + positional.push(t) + i++ + } + + return { positional, opts } +} + +export function hasOpt(opts: Map, ...names: string[]): boolean { + for (const n of names) { + const v = opts.get(n.toLowerCase()) + if (v === true) return true + } + return false +} + +export function getOpt(opts: Map, ...names: string[]): string | undefined { + for (const n of names) { + const v = opts.get(n.toLowerCase()) + if (v !== undefined && v !== true) return v + } + return undefined +} + +export function wantsCliHelp(parsed: ParsedCli): boolean { + return hasOpt(parsed.opts, 'help', 'h') +} + +export function rejectUnknownOptions( + parsed: ParsedCli, + allowed: ReadonlySet, + commandName: string +): CliResult | null { + for (const k of parsed.opts.keys()) { + if (k === 'help' || k === 'h') continue + if (!allowed.has(k)) { + return { code: 1, out: '', err: `${commandName}: unknown option --${k}\n` } + } + } + return null +} diff --git a/KeeperSdk/src/cli/registry.ts b/KeeperSdk/src/cli/registry.ts new file mode 100644 index 00000000..4e541bac --- /dev/null +++ b/KeeperSdk/src/cli/registry.ts @@ -0,0 +1,55 @@ +import type { CliCommandDefinition } from './types' + +const commands = new Map() +const aliases = new Map() + +function normalizeName(name: string): string { + return name.toLowerCase() +} + +export function registerCliCommand(def: CliCommandDefinition): void { + const key = normalizeName(def.name) + commands.set(key, def) + if (def.aliases) { + for (const alias of def.aliases) { + aliases.set(normalizeName(alias), key) + } + } +} + +export function registerCliAlias(alias: string, commandName: string): void { + aliases.set(normalizeName(alias), normalizeName(commandName)) +} + +export function resolveCliCommandName(name: string): string | undefined { + const key = normalizeName(name) + if (commands.has(key)) return key + return aliases.get(key) +} + +export function getCliCommand(name: string): CliCommandDefinition | undefined { + const key = resolveCliCommandName(name) + return key ? commands.get(key) : undefined +} + +export function listCliCommands(): CliCommandDefinition[] { + return [...commands.values()].sort((a, b) => { + const oa = a.order ?? 500 + const ob = b.order ?? 500 + if (oa !== ob) return oa - ob + return a.name.localeCompare(b.name) + }) +} + +export function listCliCommandNames(): readonly string[] { + return listCliCommands().map((c) => c.name) +} + +export function listDocumentedCommands(): readonly string[] { + return listCliCommandNames() +} + +export function clearCliRegistry(): void { + commands.clear() + aliases.clear() +} diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts new file mode 100644 index 00000000..8b175282 --- /dev/null +++ b/KeeperSdk/src/cli/types.ts @@ -0,0 +1,62 @@ +import type { DRecord, DSharedFolder } from '@keeper-security/keeperapi' + +export type CliResult = { + code: number + out: string + err: string + /** Login needs masked password from host UI (never in CLI line). */ + needPassword?: boolean + loginUsername?: string +} + +export type ParsedCli = { + positional: string[] + opts: Map +} + +/** Vault surface used by CLI command handlers. */ +export type KeeperCliVault = { + readonly isLoggedIn: boolean + login(username: string, password: string): Promise + loginWithSessionToken(username: string, sessionToken: string): Promise + logout(): Promise + sync(): Promise + getRecords(): DRecord[] + getSharedFolders(): DSharedFolder[] + registerDevice(deviceToken: string, privateKey: string, options?: { username?: string }): Promise +} + +/** Host adapter (browser shell, Node Commander, tests). */ +export type KeeperCliHost = { + getVault(): KeeperCliVault + envString(name: string): string | undefined + formatError(context: string, err: unknown): string +} + +export type CliHelpDoc = { + title: string + synopsis?: string + description?: string + arguments?: string + options?: string + environment?: string + keeperSdk?: string + seeAlso?: string + note?: string + /** Append standard KeeperVault API overview (login, records, folders, …). */ + appendVaultSurface?: boolean +} + +export type CliCommandDefinition = { + name: string + order?: number + description: string + usage: string + aliases?: readonly string[] + subcommands?: readonly string[] + flagOptions?: readonly string[] + /** If set, unknown options are rejected (excluding help). */ + allowedOptions?: ReadonlySet + help: CliHelpDoc + run: (host: KeeperCliHost, parsed: ParsedCli) => Promise +} diff --git a/KeeperSdk/src/cli/utils.ts b/KeeperSdk/src/cli/utils.ts new file mode 100644 index 00000000..5db5dae4 --- /dev/null +++ b/KeeperSdk/src/cli/utils.ts @@ -0,0 +1,18 @@ +export function utf8ToBase64Url(s: string): string { + const bytes = new TextEncoder().encode(s) + let b64: string + if (typeof Buffer !== 'undefined') { + b64 = Buffer.from(bytes).toString('base64') + } else { + let bin = '' + for (let i = 0; i < bytes.length; i++) { + bin += String.fromCharCode(bytes[i]!) + } + b64 = globalThis.btoa(bin) + } + return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +export function recordUid(rec: { uid?: string }): string { + return rec.uid || '(unknown uid)' +} diff --git a/KeeperSdk/src/cli/vaultSurface.ts b/KeeperSdk/src/cli/vaultSurface.ts new file mode 100644 index 00000000..2c7e65ba --- /dev/null +++ b/KeeperSdk/src/cli/vaultSurface.ts @@ -0,0 +1,18 @@ +/** Shared footer for vault-related command help (SDK APIs not yet exposed as CLI). */ +export const KEEPER_VAULT_SURFACE = ` +KeeperVault (JavaScript SDK) — operations available in code (not all exposed as CLI yet): + + Session: login, loginWithSessionToken, logout, resumeSession, sync, disconnect, registerDevice + Records: getRecords, findRecord, findRecords, getRecordByUid, getRecordsByType, + addRecord, updateRecord, deleteRecord, moveRecord, getRecordHistory, + printRecords + Sharing: shareRecord, removeRecordShare, getRecordShareInfo + Folders: listFolder, changeDirectory, getFolder, mkdir, addFolder, updateFolder, + renameFolder, deleteFolder, rmdir, tree, getCurrentFolderUid + Shared folders: getSharedFolders, listSharedFolders, shareFolder, … + Teams / metadata: getTeams, getRecordMetadata, getSummary, … + +Utilities exported from @keeper-security/keeper-sdk-javascript include searchRecords, +formatRecord, getRecordTitle, getRecordPassword, getRecordLogin, shareRecord, … +See the SDK package for full APIs. +`.trim() diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 5e93ce18..135843d1 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -1,12 +1,13 @@ +import { connectSdkPlatform } from './platform' +import { nodeSdkPlatform } from './platform/node/platform' + +connectSdkPlatform(nodeSdkPlatform) + +export * from './api' + +/** Node.js Commander / CLI helpers (readline, ~/.keeper config). */ export { ConsoleAuthUI } from './auth/ConsoleAuthUI' -export { SessionManager, FileConfigLoader } from './auth/SessionManager' -export type { - KeeperJsonConfig, - ConfigLoader, - ConfigurationUser, - ConfigurationServerConfig, - ConfigurationDeviceConfig, -} from './auth/SessionManager' +export { FileConfigLoader } from './auth/node/FileConfigLoader' export { login, cleanup, prompt, suppressLogs, loadKeeperConfig, resolveServer } from './auth/ConsoleLogin' export { InMemoryStorage } from './storage/InMemoryStorage' diff --git a/KeeperSdk/src/platform/browser/platform.ts b/KeeperSdk/src/platform/browser/platform.ts new file mode 100644 index 00000000..5a0e639e --- /dev/null +++ b/KeeperSdk/src/platform/browser/platform.ts @@ -0,0 +1,70 @@ +import type { AuthUI3 } from '@keeper-security/keeperapi' +import { UnavailableAuthUI } from '../../auth/UnavailableAuthUI' +import { KeeperSdkError, ResultCodes } from '../../utils' +import type { ConfigLoader } from '../../auth/config' +import * as asmCrypto from 'asmcrypto.js' +import type { SdkPlatform, SdkReadline } from '../types' + +type AsmCryptoModule = typeof asmCrypto & { + HMAC: { sign: (data: Uint8Array, key: Uint8Array, hash: unknown) => Uint8Array } + SHA1: unknown + SHA256: unknown + SHA512: unknown +} + +const asm = asmCrypto as AsmCryptoModule + +const HMAC_HASH = { + sha1: asm.SHA1, + sha256: asm.SHA256, + sha512: asm.SHA512, +} as const + +const BROWSER_READLINE_MSG = + 'Interactive readline is not available in the browser. Use keeper-shell password transport or a custom authUI.' + +const BROWSER_FILE_CONFIG_MSG = + 'File-based Keeper config (~/.keeper) is not available in the browser. Pass an in-memory ConfigLoader to SessionManager or KeeperVault.sessionStorage.' + +class BrowserReadline implements SdkReadline { + question(_prompt: string): Promise { + return Promise.reject(new KeeperSdkError(BROWSER_READLINE_MSG, ResultCodes.USER_CANCELLED)) + } + close(): void { + /* noop */ + } +} + +export const browserSdkPlatform: SdkPlatform = { + runtime: 'browser', + + delay(ms: number): Promise { + return new Promise((resolve) => globalThis.setTimeout(resolve, ms)) + }, + + createReadline(): SdkReadline { + return new BrowserReadline() + }, + + hmac(algorithm, key, data) { + const hash = HMAC_HASH[algorithm] + if (!hash) { + throw new KeeperSdkError(`Unsupported HMAC algorithm: ${algorithm}`, ResultCodes.UNSUPPORTED_2FA_CHANNEL) + } + return asm.HMAC.sign(data, key, hash) + }, + + createFileConfigLoader(): ConfigLoader { + throw new KeeperSdkError(BROWSER_FILE_CONFIG_MSG, ResultCodes.NOT_LOGGED_IN) + }, + + createAuthUI(useConsoleAuth: boolean): AuthUI3 { + if (useConsoleAuth) { + throw new KeeperSdkError( + 'ConsoleAuthUI (readline) is not available in the browser. Set useConsoleAuth: false and provide authUI, or use keeper-shell.', + ResultCodes.USER_CANCELLED + ) + } + return new UnavailableAuthUI() + }, +} diff --git a/KeeperSdk/src/platform/index.ts b/KeeperSdk/src/platform/index.ts new file mode 100644 index 00000000..32bac290 --- /dev/null +++ b/KeeperSdk/src/platform/index.ts @@ -0,0 +1,20 @@ +import type { SdkPlatform } from './types' + +let active: SdkPlatform | undefined + +export type { SdkPlatform, SdkReadline, SdkRuntime } from './types' + +export function connectSdkPlatform(platform: SdkPlatform): void { + active = platform +} + +export function getSdkPlatform(): SdkPlatform { + if (!active) { + throw new Error('Keeper SDK platform is not initialized. Import @keeper-security/keeper-sdk-javascript or /browser entry first.') + } + return active +} + +export function isSdkPlatformConnected(): boolean { + return active !== undefined +} diff --git a/KeeperSdk/src/platform/node/platform.ts b/KeeperSdk/src/platform/node/platform.ts new file mode 100644 index 00000000..9a3a9b60 --- /dev/null +++ b/KeeperSdk/src/platform/node/platform.ts @@ -0,0 +1,42 @@ +import { createHmac } from 'crypto' +import readline from 'readline/promises' +import type { AuthUI3 } from '@keeper-security/keeperapi' +import { ConsoleAuthUI } from '../../auth/ConsoleAuthUI' +import { UnavailableAuthUI } from '../../auth/UnavailableAuthUI' +import { FileConfigLoader } from '../../auth/node/FileConfigLoader' +import type { ConfigLoader } from '../../auth/config' +import type { SdkPlatform, SdkReadline } from '../types' + +function nodeReadline(input?: unknown, output?: unknown): SdkReadline { + const rl = readline.createInterface({ + input: (input ?? process.stdin) as NodeJS.ReadableStream, + output: (output ?? process.stdout) as NodeJS.WritableStream, + }) + return { + question: (prompt) => rl.question(prompt), + close: () => rl.close(), + } +} + +export const nodeSdkPlatform: SdkPlatform = { + runtime: 'node', + + delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) + }, + + createReadline: nodeReadline, + + hmac(algorithm, key, data) { + const algo = algorithm.toLowerCase() + return new Uint8Array(createHmac(algo, Buffer.from(key)).update(data).digest()) + }, + + createFileConfigLoader(configDir?: string): ConfigLoader { + return new FileConfigLoader(configDir) + }, + + createAuthUI(useConsoleAuth: boolean): AuthUI3 { + return useConsoleAuth ? new ConsoleAuthUI() : new UnavailableAuthUI() + }, +} diff --git a/KeeperSdk/src/platform/types.ts b/KeeperSdk/src/platform/types.ts new file mode 100644 index 00000000..7816e006 --- /dev/null +++ b/KeeperSdk/src/platform/types.ts @@ -0,0 +1,25 @@ +import type { AuthUI3 } from '@keeper-security/keeperapi' +import type { ConfigLoader } from '../auth/config' + +export type SdkRuntime = 'node' | 'browser' + +export interface SdkReadline { + question(prompt: string): Promise + close(): void +} + +/** Small platform surface for KeeperSdk (auth UI, config, TOTP, timers). */ +export interface SdkPlatform { + readonly runtime: SdkRuntime + + delay(ms: number): Promise + + createReadline(input?: unknown, output?: unknown): SdkReadline + + /** HMAC digest (e.g. SHA-1 for TOTP). */ + hmac(algorithm: 'sha1' | 'sha256' | 'sha512', key: Uint8Array, data: Uint8Array): Uint8Array + + createFileConfigLoader(configDir?: string): ConfigLoader + + createAuthUI(useConsoleAuth: boolean): AuthUI3 +} diff --git a/KeeperSdk/src/records/Totp.ts b/KeeperSdk/src/records/Totp.ts index f0b0f31e..a75a2fa7 100644 --- a/KeeperSdk/src/records/Totp.ts +++ b/KeeperSdk/src/records/Totp.ts @@ -1,4 +1,4 @@ -import { createHmac } from 'crypto' +import { getSdkPlatform } from '../platform' export type TotpAlgorithm = 'SHA1' | 'SHA256' | 'SHA512' @@ -98,9 +98,8 @@ export function getTotpCode(urlOrParams: string | TotpParams, now: number = Date const counter = Math.floor(seconds / params.period) const secondsRemaining = params.period - (seconds % params.period) - const digest = createHmac(params.algorithm.toLowerCase(), Buffer.from(key)) - .update(counterToBuffer(counter)) - .digest() + const algo = params.algorithm.toLowerCase() as 'sha1' | 'sha256' | 'sha512' + const digest = getSdkPlatform().hmac(algo, key, counterToBuffer(counter)) if (digest.length === 0) return null const offset = digest[digest.length - 1] & 0x0f diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 7ed7726f..c5424897 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -13,7 +13,7 @@ import { import type { SyncResult, SyncLogFormat, VaultStorage, SessionStorage, AuthUI3 } from '@keeper-security/keeperapi' import { InMemoryStorage } from '../storage/InMemoryStorage' import { SessionManager } from '../auth/SessionManager' -import { ConsoleAuthUI } from '../auth/ConsoleAuthUI' +import { getSdkPlatform } from '../platform' import { searchRecords, formatRecord, getRecordTitle, getRecordType } from '../records/RecordUtils' import { addRecord as addRecordOp, @@ -158,7 +158,7 @@ export class KeeperVault { this.log = new ConsoleLogger(this.config.logLevel) this.storage = config?.storage || new InMemoryStorage() this.sessionManager = config?.sessionStorage || new SessionManager(this.config.configDir || undefined) - this.authUI = config?.authUI || new ConsoleAuthUI() + this.authUI = config?.authUI ?? getSdkPlatform().createAuthUI(this.config.useConsoleAuth) const authProvider = () => this.getAuthOrThrow() this.folderManager = new FolderManager(this.storage, this.folderSession, authProvider) diff --git a/shellcomponent/package-lock.json b/shellcomponent/package-lock.json index 6c84bd94..a100bf29 100644 --- a/shellcomponent/package-lock.json +++ b/shellcomponent/package-lock.json @@ -1,12 +1,12 @@ { "name": "@keeper-security/keeper-shell-component", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@keeper-security/keeper-shell-component", - "version": "1.0.0", + "version": "1.0.1", "license": "ISC", "dependencies": { "@keeper-security/keeper-sdk-javascript": "file:../KeeperSdk", @@ -55,9 +55,10 @@ }, "../KeeperSdk": { "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@keeper-security/keeperapi": "file:../keeperapi", + "asmcrypto.js": "^2.3.2", "typescript": "^4.6.3" }, "devDependencies": { @@ -540,13 +541,12 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "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", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -556,9 +556,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { @@ -580,9 +580,9 @@ "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], @@ -594,9 +594,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], @@ -608,9 +608,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -622,9 +622,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -636,9 +636,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], @@ -650,9 +650,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -664,9 +664,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], @@ -678,9 +678,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], @@ -692,9 +692,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], @@ -706,9 +706,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], @@ -720,9 +720,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ "loong64" ], @@ -734,9 +734,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ "loong64" ], @@ -748,9 +748,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ "ppc64" ], @@ -762,9 +762,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ "ppc64" ], @@ -776,9 +776,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], @@ -790,9 +790,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], @@ -804,9 +804,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], @@ -818,9 +818,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], @@ -832,9 +832,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], @@ -846,9 +846,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", "cpu": [ "x64" ], @@ -860,9 +860,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -874,9 +874,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -888,9 +888,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], @@ -902,9 +902,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -916,9 +916,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], @@ -1185,9 +1185,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", - "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -1195,23 +1195,23 @@ "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" } }, "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1225,31 +1225,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, diff --git a/shellcomponent/src/cli/cliCommandDocs.ts b/shellcomponent/src/cli/cliCommandDocs.ts deleted file mode 100644 index 76806ca8..00000000 --- a/shellcomponent/src/cli/cliCommandDocs.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Long-form `--help` text per CLI command (keeper-shell + KeeperSdk). - */ - -const KEEPER_VAULT_SURFACE = ` -KeeperVault (JavaScript SDK) — operations available in code (not all exposed as CLI yet): - - Session: login, loginWithSessionToken, logout, resumeSession, sync, disconnect, registerDevice - Records: getRecords, findRecord, findRecords, getRecordByUid, getRecordsByType, - addRecord, updateRecord, deleteRecord, moveRecord, getRecordHistory, - printRecords - Sharing: shareRecord, removeRecordShare, getRecordShareInfo - Folders: listFolder, changeDirectory, getFolder, mkdir, addFolder, updateFolder, - renameFolder, deleteFolder, rmdir, tree, getCurrentFolderUid - Shared folders: getSharedFolders, listSharedFolders, shareFolder, … - Teams / metadata: getTeams, getRecordMetadata, getSummary, … - -Utilities exported from @keeper-security/keeper-sdk-javascript include searchRecords, -formatRecord, getRecordTitle, getRecordPassword, getRecordLogin, shareRecord, … -See the SDK package for full APIs. -`.trim(); - -const DOCS: Record = { - help: ` -help — show commands or short syntax for one command - -SYNOPSIS - help [COMMAND] - -DESCRIPTION - Without arguments, lists every built-in command with a one-line summary. - With COMMAND, prints the same overview line plus usage for that command. - - For full documentation on each command, run: - COMMAND --help - COMMAND -h - -OPTIONS - None. This command does not take GNU-style flags. - -SEE ALSO - Each command’s --help output. -`.trim(), - - login: ` -login — authenticate to Keeper (vault session) - -SYNOPSIS - login [--username|--user EMAIL_OR_NAME] - login [--username|--user U] [--session-token|--token|--st TOKEN] - login [--username|--user U] [--session-token TOKEN] [--session-token-plain] - -DESCRIPTION - Establishes a Keeper session using KeeperVault inside keeper-shell (in-memory - session storage in the browser). - - Username comes from --username / --user or KEEPER_USERNAME. - - Password MUST NOT appear on the CLI line (logging, proxies, browser history). - Automation: set KEEPER_PASSWORD in the environment when embedding in Node. - Web shell: run login with only a username; the UI prompts for a masked password - and sends it through the login transport, not in "line". - - Session token login uses KeeperVault.loginWithSessionToken. The token may be - passed on the command line or via KEEPER_SESSION_TOKEN (sensitive — same - caveats as any secret on argv). - - --session-token-plain encodes the token from UTF-8 to base64url before the - SDK call (same idea as the session_token_login example when the token is raw text). - - Device registration: session token login requires deviceToken + privateKey for - this host in session storage. Use register-device (or a prior password login in - this shell) to store them; see register-device --help. - -OPTIONS - --username, --user Account identifier (often email). - --session-token, --token, --st Session token string (or use KEEPER_SESSION_TOKEN). - --session-token-plain Treat --session-token value as plain UTF-8 and encode base64url. - -ENVIRONMENT - KEEPER_USERNAME Default username if not passed on the command line. - KEEPER_PASSWORD Password for non-interactive login (no session token). - KEEPER_SESSION_TOKEN Session token when not passed as a flag. - KEEPER_HOST Optional vault host / region (also: keeper-host attribute). - -KEEPER SDK - Uses KeeperVault.login or loginWithSessionToken, then sync. resumeSession and - clone-code flows exist in the SDK but are not exposed in this CLI yet. - -${KEEPER_VAULT_SURFACE} -`.trim(), - - logout: ` -logout — end the current Keeper session - -SYNOPSIS - logout - -DESCRIPTION - Calls KeeperVault.logout when a session exists. - -OPTIONS - None. - -${KEEPER_VAULT_SURFACE} -`.trim(), - - records: ` -records — list vault records (record UID and title) - -SYNOPSIS - records [list] - -DESCRIPTION - Runs sync, then prints a table of record_uid and title for each record. - -ARGUMENTS - list Optional; default behavior is to list. Other subcommands may be added later. - -OPTIONS - --help, -h Show this help. - -SESSION - If not logged in, the CLI attempts env-based login (KEEPER_USERNAME / - KEEPER_PASSWORD). Use the masked password flow in the shell if you rely on it. - -KEEPER SDK - Maps to KeeperVault.sync(), getRecords(), getRecordTitle(). - Related APIs you can extend in this CLI later: findRecords, getRecordsByType, - addRecord, updateRecord, deleteRecord, shareRecord, getRecordHistory, … - -${KEEPER_VAULT_SURFACE} -`.trim(), - - folders: ` -folders — list shared folders - -SYNOPSIS - folders [list] - -DESCRIPTION - Runs sync, then prints shared_folder_uid and name for each shared folder. - -ARGUMENTS - list Optional; default is list. - -OPTIONS - --help, -h Show this help. - -SESSION - Same as records: uses env login if needed, or log in via the shell first. - -KEEPER SDK - Uses KeeperVault.sync(), getSharedFolders(). - Related: listSharedFolders, shareFolder, FolderManager / SharedFolderManager. - -${KEEPER_VAULT_SURFACE} -`.trim(), - - mkdir: ` -mkdir — host filesystem directory (disabled in embedded shell) - -SYNOPSIS - mkdir [-p|--parents] [--] RELATIVE_PATH - -DESCRIPTION - In keeper-shell with in-browser SDK transport, host mkdir is not available (no - sandboxed filesystem). Set api-base to an HTTP CLI backend if you need - this command, or use Keeper vault folder APIs in code. - -OPTIONS - -p, --parents (remote server only.) - -- End of options. - -NOTE - Vault folder operations live on KeeperVault (mkdir, addFolder, …) in the SDK. -`.trim(), - - "register-device": ` -register-device — store device token and private key for session-token login - -SYNOPSIS - register-device --device-token|--dt B64 --private-key|--pk B64 [--username|--user U] - -DESCRIPTION - Calls KeeperVault.registerDevice to save device credentials for the current - host in this shell’s in-memory session. After this, you can run: - - login --username YOU --session-token TOKEN - - without a prior password login in this shell, as long as the token is valid. - - Obtain device_token and private_key from another machine’s keeper config after - a successful login, or from your integration that provisions device keys. - Values accept base64 or base64url (same decoding as SessionManager / normal64Bytes). - -OPTIONS - --device-token, --dt Device token string. - --private-key, --pk Device private key string. - --username, --user Optional; sets last username in session storage (recommended). - -ENVIRONMENT - REGISTER_DEVICE_TOKEN Same as --device-token when flag omitted. - REGISTER_DEVICE_PRIVATE_KEY Same as --private-key when flag omitted. - KEEPER_HOST Same as other keeper commands. - -KEEPER SDK - KeeperVault.registerDevice(deviceToken, privateKey, { username? }) - -${KEEPER_VAULT_SURFACE} -`.trim(), -}; - -/** Long help text for `COMMAND --help`; null if unknown. */ -export function getDetailedHelpPage(command: string): string | null { - const key = command.toLowerCase(); - const body = DOCS[key]; - if (!body) return null; - return `${body}\n`; -} - -export function listDocumentedCommands(): readonly string[] { - return Object.keys(DOCS).sort(); -} diff --git a/shellcomponent/src/cli/cliComplete.ts b/shellcomponent/src/cli/cliComplete.ts index 2540c866..c2f2fece 100644 --- a/shellcomponent/src/cli/cliComplete.ts +++ b/shellcomponent/src/cli/cliComplete.ts @@ -1,53 +1,21 @@ /** - * Tab-completion metadata for the keeper-shell CLI. + * Tab-completion metadata for the keeper-shell CLI (from SDK command registry). */ -import { CLI_TOP_LEVEL_NAMES } from "./cliHelp.js"; - -const TOP_LEVEL = CLI_TOP_LEVEL_NAMES; - -const SUBCOMMANDS: Record = { - records: ["list"], - folders: ["list"], -}; +import { getCliCommand, listCliCommandNames } from "@keeper-security/keeper-sdk-javascript"; const HELP_FLAGS = ["--help", "-h"] as const; -/** Long/short flags after a command (plus universal --help / -h). */ -const FLAG_OPTIONS: Record = { - login: [ - "--user", - "--username", - "--session-token", - "--token", - "--st", - "--session-token-plain", - ], - mkdir: ["-p", "--parents"], - "register-device": [ - "--device-token", - "--dt", - "--private-key", - "--pk", - "--user", - "--username", - ], -}; - function flagsFor(cmd: string): readonly string[] { - const extra = FLAG_OPTIONS[cmd] ?? []; + const def = getCliCommand(cmd); + const extra = def?.flagOptions ?? []; return [...HELP_FLAGS, ...extra]; } export type CliCompleteResult = { - /** Line prefix to keep; replacement segment is `line.slice(base.length)`. */ base: string; - /** Suggested full tokens (each replaces the partial segment). */ candidates: string[]; }; -/** - * @param line - Current input (cursor treated as end of line). - */ export function completeCliLine(line: string): CliCompleteResult { const completesNewWord = /\s$/.test(line); const segments = line.match(/\S+/g) ?? []; @@ -73,7 +41,8 @@ export function completeCliLine(line: string): CliCompleteResult { partialLen > 0 ? line.slice(0, line.length - partialLen) : line; if (words.length === 0) { - const hits = TOP_LEVEL.filter((c) => c.startsWith(stubLc)); + const top = listCliCommandNames(); + const hits = top.filter((c) => c.startsWith(stubLc)); return { base: baseFor(stub.length), candidates: hits }; } @@ -82,16 +51,16 @@ export function completeCliLine(line: string): CliCompleteResult { if (words.length === 1) { if (stub.startsWith("-")) { const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: hits }; + return { base: baseFor(stub.length), candidates: [...hits] }; } - const subs = SUBCOMMANDS[cmd]; - if (subs) { + const subs = getCliCommand(cmd)?.subcommands; + if (subs?.length) { const hits = subs.filter((s) => lc(s).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: hits }; + return { base: baseFor(stub.length), candidates: [...hits] }; } if (completesNewWord || stub.length > 0) { const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: hits }; + return { base: baseFor(stub.length), candidates: [...hits] }; } } diff --git a/shellcomponent/src/cli/cliDispatch.ts b/shellcomponent/src/cli/cliDispatch.ts index 9e9328b0..58a25bf3 100644 --- a/shellcomponent/src/cli/cliDispatch.ts +++ b/shellcomponent/src/cli/cliDispatch.ts @@ -1,13 +1,6 @@ -import { mkdirCommand } from "./mkdir.js"; -import { - keeperLoginCommand, - keeperLogoutCommand, - keeperRecordsCommand, - keeperFoldersCommand, - registerDeviceCommand, -} from "./keeperCommands.js"; -import { helpCommand } from "./cliHelp.js"; -import { tokenizeArguments, parseCliArgs } from "./cliParse.js"; +import "./mkdirCommand.js"; +import { dispatchKeeperCli, tokenizeArguments } from "@keeper-security/keeper-sdk-javascript"; +import { shellKeeperCliHost } from "./keeperCliHost.js"; import type { CliResult } from "./types.js"; const rawMax = @@ -23,29 +16,9 @@ export async function dispatchCliLine(line: string): Promise { const tokens = tokenizeArguments(line.trim()); const name = tokens[0]?.toLowerCase(); - const rest = tokens.slice(1); - const parsed = parseCliArgs(rest); - if (!name) { return { code: 0, out: "", err: "" }; } - switch (name) { - case "help": - return helpCommand(parsed); - case "mkdir": - return mkdirCommand(parsed); - case "login": - return keeperLoginCommand(parsed); - case "logout": - return keeperLogoutCommand(parsed); - case "records": - return keeperRecordsCommand(parsed); - case "folders": - return keeperFoldersCommand(parsed); - case "register-device": - return registerDeviceCommand(parsed); - default: - return { code: 1, out: "", err: `Unknown command: ${name}\n` }; - } + return dispatchKeeperCli(name, tokens.slice(1), shellKeeperCliHost); } diff --git a/shellcomponent/src/cli/cliHelp.ts b/shellcomponent/src/cli/cliHelp.ts deleted file mode 100644 index 587de052..00000000 --- a/shellcomponent/src/cli/cliHelp.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { wantsCliHelp, type ParsedCli } from "./cliParse.js"; -import { getDetailedHelpPage } from "./cliCommandDocs.js"; -import type { CliResult } from "./types.js"; - -type CliHelpEntry = { - name: string; - usage: string; - description: string; -}; - -const CLI_COMMANDS: CliHelpEntry[] = [ - { - name: "folders", - usage: "folders [list] [--help|-h]", - description: "List shared folders in the vault (uses env or `login --username …`).", - }, - { - name: "help", - usage: "help [command] (see also: help --help)", - description: "Show all commands, or full docs for one command (same as COMMAND --help).", - }, - { - name: "login", - usage: - "login [--username|--user ] [--session-token|--token|--st ] [--session-token-plain] [--help|-h]", - description: - "Log in with password (env / masked prompt) or session token (flag or KEEPER_SESSION_TOKEN). Password never on CLI line.", - }, - { - name: "logout", - usage: "logout [--help|-h]", - description: "Log out of the current Keeper session.", - }, - { - name: "mkdir", - usage: "mkdir [-p|--parents] [--] ; mkdir --help", - description: - "Host mkdir is disabled in keeper-shell (no filesystem). Use `api-base` for an HTTP CLI backend, or SDK folder APIs.", - }, - { - name: "records", - usage: "records [list] [--help|-h]", - description: "List vault records (uid and title).", - }, - { - name: "register-device", - usage: - "register-device --device-token|--dt --private-key|--pk [--username ] [--help|-h]", - description: - "Store device token + private key in this shell’s session so session-token login works (see login --help).", - }, -].sort((a, b) => a.name.localeCompare(b.name)); - -/** Top-level names for tab completion (sorted). */ -export const CLI_TOP_LEVEL_NAMES: readonly string[] = CLI_COMMANDS.map((c) => c.name); - -function formatAllCommands(): string { - const w = Math.max(...CLI_COMMANDS.map((c) => c.name.length), 8); - let out = "Supported commands:\n\n"; - for (const c of CLI_COMMANDS) { - out += ` ${c.name.padEnd(w)} ${c.description}\n`; - } - out += "\nOptions use GNU-style syntax: `--name`, `--name=value`, short flags (`-p` or `-rf` when each letter is a switch), and `--` ends options.\n"; - out += "Quoted arguments and `\\` escapes are supported.\n\n"; - out += "Run `help ` for a short summary, or `command --help` / `command -h` for full documentation.\n"; - return out; -} - -function formatOne(name: string): CliResult { - const key = name.toLowerCase(); - const long = getDetailedHelpPage(key); - if (long) { - return { code: 0, out: long, err: "" }; - } - const c = CLI_COMMANDS.find((e) => e.name === key); - if (!c) { - return { code: 1, out: "", err: `help: unknown command: ${name}\n` }; - } - const out = `${c.name} — ${c.description}\n Usage: ${c.usage}\n`; - return { code: 0, out, err: "" }; -} - -export function helpCommand(parsed: ParsedCli): CliResult { - if (wantsCliHelp(parsed)) { - const h = getDetailedHelpPage("help"); - return { code: 0, out: h ?? "", err: "" }; - } - if (parsed.opts.size > 0) { - return { code: 1, out: "", err: "help: unknown option (try `help --help`)\n" }; - } - const args = parsed.positional; - if (args.length === 0) { - return { code: 0, out: formatAllCommands(), err: "" }; - } - if (args.length > 1) { - return { code: 1, out: "", err: "Usage: help [command]\n" }; - } - return formatOne(args[0]); -} diff --git a/shellcomponent/src/cli/cliParse.ts b/shellcomponent/src/cli/cliParse.ts deleted file mode 100644 index d46db5a5..00000000 --- a/shellcomponent/src/cli/cliParse.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Shell-like tokenization and GNU-style option parsing (similar in spirit to - * Keeper Commander / CommandLineParser on .NET: quoted args, `--`, `-abc`, `--key=value`). - */ - -export type ParsedCli = { - positional: string[]; - /** Lowercase option names; value `true` means boolean flag. */ - opts: Map; -}; - -const isWhitespace = (ch: string) => /\s/.test(ch); - -/** - * Split a command line into tokens; respects double quotes and `\` escapes. - * Mirrors the Commander Cli `TokenizeArguments` behavior in broad strokes. - */ -export function tokenizeArguments(args: string): string[] { - const out: string[] = []; - const sb: string[] = []; - let pos = 0; - let inQuote = false; - let escape = false; - - const flush = () => { - if (sb.length > 0) { - out.push(sb.join("")); - sb.length = 0; - } - }; - - while (pos < args.length) { - const ch = args[pos]; - if (escape) { - escape = false; - sb.push(ch); - pos++; - continue; - } - if (inQuote) { - if (ch === "\\") { - escape = true; - pos++; - continue; - } - if (ch === '"') { - inQuote = false; - pos++; - continue; - } - sb.push(ch); - pos++; - continue; - } - switch (ch) { - case "\\": - escape = true; - pos++; - break; - case '"': - inQuote = true; - pos++; - break; - default: - if (isWhitespace(ch)) { - flush(); - pos++; - } else { - sb.push(ch); - pos++; - } - } - } - flush(); - return out; -} - -function setBool(opts: Map, k: string): void { - opts.set(k.toLowerCase(), true); -} - -function setStr(opts: Map, k: string, v: string): void { - opts.set(k.toLowerCase(), v); -} - -/** - * Parse argv-style tokens after the command name: GNU-like long/short options and `--`. - */ -export function parseCliArgs(tokens: string[]): ParsedCli { - const positional: string[] = []; - const opts = new Map(); - - let i = 0; - while (i < tokens.length) { - const t = tokens[i]; - if (t === "--") { - positional.push(...tokens.slice(i + 1)); - break; - } - if (t === "-" || !t.startsWith("-")) { - positional.push(t); - i++; - continue; - } - - if (t.startsWith("--")) { - const body = t.slice(2); - if (!body) { - positional.push(t); - i++; - continue; - } - const eq = body.indexOf("="); - if (eq >= 0) { - setStr(opts, body.slice(0, eq), body.slice(eq + 1)); - i++; - continue; - } - const name = body; - const next = tokens[i + 1]; - if (next && next !== "--" && !next.startsWith("-")) { - setStr(opts, name, next); - i += 2; - continue; - } - setBool(opts, name); - i++; - continue; - } - - const rest = t.slice(1); - if (!rest) { - positional.push(t); - i++; - continue; - } - if (/^[A-Za-z]$/.test(rest)) { - setBool(opts, rest); - i++; - continue; - } - if (/^[A-Za-z]+$/.test(rest)) { - for (const ch of rest) setBool(opts, ch); - i++; - continue; - } - - positional.push(t); - i++; - } - - return { positional, opts }; -} - -export function hasOpt(opts: Map, ...names: string[]): boolean { - for (const n of names) { - const v = opts.get(n.toLowerCase()); - if (v === true) return true; - } - return false; -} - -export function getOpt(opts: Map, ...names: string[]): string | undefined { - for (const n of names) { - const v = opts.get(n.toLowerCase()); - if (v !== undefined && v !== true) return v; - } - return undefined; -} - -/** True if `-h` / `--help` was passed (boolean flags only). */ -export function wantsCliHelp(parsed: ParsedCli): boolean { - return hasOpt(parsed.opts, "help", "h"); -} diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts new file mode 100644 index 00000000..89b68c4a --- /dev/null +++ b/shellcomponent/src/cli/keeperCliHost.ts @@ -0,0 +1,98 @@ +import type { KeeperCliHost, KeeperCliVault } from "@keeper-security/keeper-sdk-javascript"; +import * as KeeperSdk from "@keeper-security/keeper-sdk-javascript"; +import { envString, getShellKeeperHost } from "./cliContext.js"; +import { InMemoryConfigLoader } from "./inMemoryConfigLoader.js"; + +const { KeeperVault, LogLevel, SessionManager, SdkDefaults } = KeeperSdk; + +type VaultInstance = InstanceType; + +let vault: VaultInstance | null = null; + +function getVault(): VaultInstance { + if (!vault) { + const host = getShellKeeperHost(); + vault = new KeeperVault({ + ...(host ? { host } : {}), + useConsoleAuth: false, + logLevel: LogLevel.WARN, + sessionStorage: new SessionManager(new InMemoryConfigLoader()), + }); + if (import.meta.env?.DEV === true) { + const devDevice = + import.meta.env.VITE_KEEPER_DEV_DEVICE_USER && + import.meta.env.VITE_KEEPER_DEV_DEVICE_TOKEN && + import.meta.env.VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY; + console.info("[keeper-shell dev] KeeperVault created", { + host: host || "(SDK default; US prod host if unset)", + clientVersion: SdkDefaults.CLIENT_VERSION, + useConsoleAuth: false, + logLevel: "WARN", + preseededDeviceFromEnv: Boolean(devDevice), + }); + } + } + return vault; +} + +export function resetShellVault(): void { + vault = null; +} + +function errMsg(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +function formatKeeperClientError(context: string, e: unknown): string { + const base = errMsg(e); + const inBrowser = typeof document !== "undefined"; + if (!inBrowser) { + return `${context}: ${base}\n`; + } + const low = base.toLowerCase(); + const fetchish = + low.includes("failed to fetch") || + low.includes("networkerror") || + low.includes("load failed") || + low.includes("network request failed"); + if (!fetchish) { + return `${context}: ${base}\n`; + } + const host = getShellKeeperHost(); + const hostHint = host + ? `\n Region/host in use: ${host} — wrong region can look like a network error; adjust keeper-host or KEEPER_HOST if needed.` + : `\n Region/host: default production (US). EU/AU/CA/JP tenants often need keeper-host on or KEEPER_HOST.`; + return ( + `${context}: ${base}\n` + + ` Why only this text: browsers intentionally hide the underlying HTTP status / CORS detail for many failed requests.\n` + + ` Typical causes (your password may still be correct):\n` + + ` • CORS — Keeper’s API may not allow calls from this page’s origin (common for http://localhost in dev).\n` + + ` • Network — offline, DNS, VPN, corporate proxy, or firewall blocking HTTPS to Keeper.\n` + + ` • Mixed content — page is http while the API is https; load the dev page over https.\n` + + hostHint + + `\n Mitigation: run login from a backend your page trusts (set web-console remote + api-base), or use the SDK from Node.js.\n` + ); +} + +function asCliVault(v: VaultInstance): KeeperCliVault { + return { + get isLoggedIn() { + return v.isLoggedIn; + }, + login: (u, p) => v.login(u, p), + loginWithSessionToken: (u, t) => v.loginWithSessionToken(u, t), + logout: () => v.logout(), + sync: async () => { + await v.sync(); + }, + getRecords: () => v.getRecords(), + getSharedFolders: () => v.getSharedFolders(), + registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), + }; +} + +export const shellKeeperCliHost: KeeperCliHost = { + getVault: () => asCliVault(getVault()), + envString, + formatError: formatKeeperClientError, +}; diff --git a/shellcomponent/src/cli/keeperCommands.ts b/shellcomponent/src/cli/keeperCommands.ts index d9b27f94..419c9141 100644 --- a/shellcomponent/src/cli/keeperCommands.ts +++ b/shellcomponent/src/cli/keeperCommands.ts @@ -1,125 +1,17 @@ /** - * Maps parsed CLI lines to KeeperSdk (KeeperVault) — browser-friendly (no Node Buffer/path). + * Shell exports for Keeper CLI login helpers (used by UI password transport). */ -import type { DRecord, DSharedFolder } from "@keeper-security/keeper-sdk-javascript"; -import * as KeeperSdk from "@keeper-security/keeper-sdk-javascript"; -import { getDetailedHelpPage } from "./cliCommandDocs.js"; -import { envString, getShellKeeperHost } from "./cliContext.js"; -import { InMemoryConfigLoader } from "./inMemoryConfigLoader.js"; -import { getOpt, hasOpt, wantsCliHelp, type ParsedCli } from "./cliParse.js"; -import type { CliResult } from "./types.js"; +import { + loginWithCredentials as sdkLoginWithCredentials, + loginWithSessionToken as sdkLoginWithSessionToken, + type CliResult, +} from "@keeper-security/keeper-sdk-javascript"; +import { shellKeeperCliHost } from "./keeperCliHost.js"; -const { KeeperVault, LogLevel, SessionManager, getRecordTitle, SdkDefaults } = KeeperSdk; +export { resetShellVault } from "./keeperCliHost.js"; -type VaultInstance = InstanceType; - -const LOGIN_OPT_NAMES = new Set([ - "username", - "user", - "session-token", - "token", - "st", - "session-token-plain", -]); - -let vault: VaultInstance | null = null; - -function utf8ToBase64Url(s: string): string { - const bytes = new TextEncoder().encode(s); - let bin = ""; - for (let i = 0; i < bytes.length; i++) { - bin += String.fromCharCode(bytes[i]!); - } - const b64 = btoa(bin); - return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -} - -function getVault(): VaultInstance { - if (!vault) { - const host = getShellKeeperHost(); - vault = new KeeperVault({ - ...(host ? { host } : {}), - useConsoleAuth: false, - logLevel: LogLevel.WARN, - sessionStorage: new SessionManager(new InMemoryConfigLoader()), - }); - if (import.meta.env?.DEV === true) { - const devDevice = - import.meta.env.VITE_KEEPER_DEV_DEVICE_USER && - import.meta.env.VITE_KEEPER_DEV_DEVICE_TOKEN && - import.meta.env.VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY; - console.info("[keeper-shell dev] KeeperVault created", { - host: host || "(SDK default; US prod host if unset)", - clientVersion: SdkDefaults.CLIENT_VERSION, - useConsoleAuth: false, - logLevel: "WARN", - preseededDeviceFromEnv: Boolean(devDevice), - }); - } - } - return vault; -} - -/** Reset vault singleton (e.g. after keeper-host attribute change). */ -export function resetShellVault(): void { - vault = null; -} - -function recordUid(rec: { uid?: string }): string { - return rec.uid || "(unknown uid)"; -} - -function errMsg(e: unknown): string { - return e instanceof Error ? e.message : String(e); -} - -/** - * Enriches errors where the browser only reports "Failed to fetch" (no status/CORS detail). - */ -function formatKeeperClientError(context: string, e: unknown): string { - const base = errMsg(e); - const inBrowser = typeof document !== "undefined"; - if (!inBrowser) { - return `${context}: ${base}\n`; - } - const low = base.toLowerCase(); - const fetchish = - low.includes("failed to fetch") || - low.includes("networkerror") || - low.includes("load failed") || - low.includes("network request failed"); - if (!fetchish) { - return `${context}: ${base}\n`; - } - const host = getShellKeeperHost(); - const hostHint = host - ? `\n Region/host in use: ${host} — wrong region can look like a network error; adjust keeper-host or KEEPER_HOST if needed.` - : `\n Region/host: default production (US). EU/AU/CA/JP tenants often need keeper-host on or KEEPER_HOST.`; - return ( - `${context}: ${base}\n` + - ` Why only this text: browsers intentionally hide the underlying HTTP status / CORS detail for many failed requests.\n` + - ` Typical causes (your password may still be correct):\n` + - ` • CORS — Keeper’s API may not allow calls from this page’s origin (common for http://localhost in dev).\n` + - ` • Network — offline, DNS, VPN, corporate proxy, or firewall blocking HTTPS to Keeper.\n` + - ` • Mixed content — page is http while the API is https; load the dev page over https.\n` + - hostHint + - `\n Mitigation: run login from a backend your page trusts (set web-console remote + api-base), or use the SDK from Node.js.\n` - ); -} - -/** Shared login used by CLI and shell password transport (password never in `line`). */ export async function loginWithCredentials(username: string, password: string): Promise { - try { - const v = getVault(); - if (v.isLoggedIn) { - await v.logout(); - } - await v.login(username, password); - await v.sync(); - return { code: 0, out: `keeper: logged in as ${username}.\n`, err: "" }; - } catch (e) { - return { code: 1, out: "", err: formatKeeperClientError("keeper", e) }; - } + return sdkLoginWithCredentials(shellKeeperCliHost, username, password); } export async function loginWithSessionTokenCredentials( @@ -127,242 +19,5 @@ export async function loginWithSessionTokenCredentials( sessionToken: string, options?: { plainToken?: boolean } ): Promise { - let token = sessionToken.trim(); - if (options?.plainToken && token.length > 0) { - token = utf8ToBase64Url(token); - } - try { - const v = getVault(); - if (v.isLoggedIn) { - await v.logout(); - } - await v.loginWithSessionToken(username, token); - await v.sync(); - return { code: 0, out: `keeper: logged in as ${username} (session token).\n`, err: "" }; - } catch (e) { - return { code: 1, out: "", err: formatKeeperClientError("keeper", e) }; - } -} - -export async function keeperLoginCommand(parsed?: ParsedCli): Promise { - const opts = parsed?.opts ?? new Map(); - if (parsed && wantsCliHelp(parsed)) { - return { code: 0, out: getDetailedHelpPage("login") ?? "", err: "" }; - } - if (parsed) { - for (const secretFlag of ["password", "pass", "pwd"] as const) { - if (opts.has(secretFlag)) { - return { - code: 1, - out: "", - err: - "login: do not pass --password on the command line (it is logged and visible). " + - "Use KEEPER_PASSWORD for automation, or run `login --username …` in the shell and enter the password when prompted (masked).\n", - }; - } - } - for (const k of opts.keys()) { - if (!LOGIN_OPT_NAMES.has(k)) { - return { code: 1, out: "", err: `login: unknown option --${k}\n` }; - } - } - } - - const username = getOpt(opts, "username", "user") ?? envString("KEEPER_USERNAME"); - const passwordEnv = envString("KEEPER_PASSWORD"); - const sessionRaw = getOpt(opts, "session-token", "token", "st") ?? envString("KEEPER_SESSION_TOKEN"); - const sessionPlain = parsed && hasOpt(opts, "session-token-plain"); - - if (parsed) { - const stPlainVal = opts.get("session-token-plain"); - if (stPlainVal !== undefined && stPlainVal !== true) { - return { - code: 1, - out: "", - err: "login: --session-token-plain is a boolean flag (no value)\n", - }; - } - } - - if (!username) { - return { - code: 1, - out: "", - err: "login: provide --username or KEEPER_USERNAME.\n", - }; - } - - const sessionTrimmed = typeof sessionRaw === "string" ? sessionRaw.trim() : ""; - if (sessionTrimmed.length > 0) { - return loginWithSessionTokenCredentials(username, sessionTrimmed, { - plainToken: !!sessionPlain, - }); - } - - if (!passwordEnv) { - return { - code: 1, - needPassword: true, - loginUsername: username, - out: "", - err: "", - }; - } - - return loginWithCredentials(username, passwordEnv); -} - -export async function keeperLogoutCommand(parsed?: ParsedCli): Promise { - if (parsed && wantsCliHelp(parsed)) { - return { code: 0, out: getDetailedHelpPage("logout") ?? "", err: "" }; - } - if (parsed && parsed.opts.size > 0) { - return { code: 1, out: "", err: "logout: no options (try: logout --help)\n" }; - } - if (parsed && parsed.positional.length > 0) { - return { code: 1, out: "", err: "Usage: logout\n" }; - } - try { - const v = getVault(); - if (!v.isLoggedIn) { - return { code: 0, out: "keeper: already logged out.\n", err: "" }; - } - await v.logout(); - return { code: 0, out: "keeper: logged out.\n", err: "" }; - } catch (e) { - return { code: 1, out: "", err: formatKeeperClientError("keeper", e) }; - } -} - -export async function keeperRecordsCommand(parsed: ParsedCli): Promise { - if (wantsCliHelp(parsed)) { - return { code: 0, out: getDetailedHelpPage("records") ?? "", err: "" }; - } - if (parsed.opts.size > 0) { - return { code: 1, out: "", err: "records: unknown option (try: records --help)\n" }; - } - const sub = parsed.positional[0]?.toLowerCase(); - if (parsed.positional.length > 1) { - return { code: 1, out: "", err: "Usage: records [list]\n" }; - } - if (sub && sub !== "list") { - return { code: 1, out: "", err: "Usage: records [list]\n" }; - } - try { - const v = getVault(); - if (!v.isLoggedIn) { - const r = await keeperLoginCommand(); - if (r.code !== 0) return r; - } - await v.sync(); - const records = v.getRecords(); - const rows = records.map((r: DRecord) => `${recordUid(r)}\t${getRecordTitle(r)}`); - const header = "record_uid\ttitle\n"; - const body = rows.length ? rows.join("\n") + "\n" : "(no records)\n"; - return { code: 0, out: header + body, err: "" }; - } catch (e) { - return { code: 1, out: "", err: formatKeeperClientError("records", e) }; - } -} - -export async function keeperFoldersCommand(parsed: ParsedCli): Promise { - if (wantsCliHelp(parsed)) { - return { code: 0, out: getDetailedHelpPage("folders") ?? "", err: "" }; - } - if (parsed.opts.size > 0) { - return { code: 1, out: "", err: "folders: unknown option (try: folders --help)\n" }; - } - const sub = parsed.positional[0]?.toLowerCase(); - if (parsed.positional.length > 1) { - return { code: 1, out: "", err: "Usage: folders [list]\n" }; - } - if (sub && sub !== "list") { - return { code: 1, out: "", err: "Usage: folders [list]\n" }; - } - try { - const v = getVault(); - if (!v.isLoggedIn) { - const r = await keeperLoginCommand(); - if (r.code !== 0) return r; - } - await v.sync(); - const folders = v.getSharedFolders(); - const rows = folders.map((f: DSharedFolder) => { - const name = f.name ?? "(unnamed)"; - const uid = f.uid ?? "(unknown uid)"; - return `${uid}\t${name}`; - }); - const header = "shared_folder_uid\tname\n"; - const body = rows.length ? rows.join("\n") + "\n" : "(no shared folders)\n"; - return { code: 0, out: header + body, err: "" }; - } catch (e) { - return { code: 1, out: "", err: formatKeeperClientError("folders", e) }; - } -} - -const REGISTER_DEVICE_OPTS = new Set([ - "device-token", - "dt", - "private-key", - "pk", - "username", - "user", -]); - -export async function registerDeviceCommand(parsed: ParsedCli): Promise { - if (wantsCliHelp(parsed)) { - return { code: 0, out: getDetailedHelpPage("register-device") ?? "", err: "" }; - } - for (const k of parsed.opts.keys()) { - if (!REGISTER_DEVICE_OPTS.has(k)) { - return { code: 1, out: "", err: `register-device: unknown option --${k}\n` }; - } - } - if (parsed.positional.length > 0) { - return { - code: 1, - out: "", - err: "register-device: unexpected positional arguments\n", - }; - } - - const deviceToken = getOpt(parsed.opts, "device-token", "dt") ?? envString("REGISTER_DEVICE_TOKEN"); - const privateKey = getOpt(parsed.opts, "private-key", "pk") ?? envString("REGISTER_DEVICE_PRIVATE_KEY"); - const usernameOpt = getOpt(parsed.opts, "username", "user"); - const username = usernameOpt?.trim() || undefined; - - const dt = typeof deviceToken === "string" ? deviceToken.trim() : ""; - const pk = typeof privateKey === "string" ? privateKey.trim() : ""; - if (!dt || !pk) { - return { - code: 1, - out: "", - err: - "register-device: --device-token and --private-key required " + - "(or REGISTER_DEVICE_TOKEN / REGISTER_DEVICE_PRIVATE_KEY env).\n", - }; - } - - try { - const v = getVault(); - type RegisterFn = (dt: string, pk: string, o?: { username?: string }) => Promise; - const registerDevice = (v as { registerDevice?: RegisterFn }).registerDevice; - if (typeof registerDevice !== "function") { - return { - code: 1, - out: "", - err: - "register-device: not available in this SDK version. Upgrade @keeper-security/keeper-sdk-javascript.\n", - }; - } - await registerDevice.call(v, dt, pk, username ? { username } : undefined); - const umsg = username ? ` Last username set to ${username}.` : ""; - return { - code: 0, - out: `keeper: device credentials stored in this shell’s session.${umsg} Next: login --username … --session-token …\n`, - err: "", - }; - } catch (e) { - return { code: 1, out: "", err: formatKeeperClientError("register-device", e) }; - } + return sdkLoginWithSessionToken(shellKeeperCliHost, username, sessionToken, options); } diff --git a/shellcomponent/src/cli/mkdir.ts b/shellcomponent/src/cli/mkdir.ts deleted file mode 100644 index b9b326e5..00000000 --- a/shellcomponent/src/cli/mkdir.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getDetailedHelpPage } from "./cliCommandDocs.js"; -import { wantsCliHelp, type ParsedCli } from "./cliParse.js"; -import type { CliResult } from "./types.js"; - -const MKDIR_OPTS = new Set(["p", "parents"]); - -export async function mkdirCommand(parsed: ParsedCli): Promise { - if (wantsCliHelp(parsed)) { - const doc = getDetailedHelpPage("mkdir"); - return { code: 0, out: doc ?? "", err: "" }; - } - for (const k of parsed.opts.keys()) { - if (!MKDIR_OPTS.has(k)) { - return { code: 1, out: "", err: `mkdir: unknown option --${k} (try: mkdir --help)\n` }; - } - } - - return { - code: 1, - out: "", - err: - "mkdir: not available in keeper-shell (no host filesystem). " + - "Set api-base to an HTTP CLI backend for workspace mkdir, or use Keeper vault folder APIs.\n", - }; -} diff --git a/shellcomponent/src/cli/mkdirCommand.ts b/shellcomponent/src/cli/mkdirCommand.ts new file mode 100644 index 00000000..f0420ee7 --- /dev/null +++ b/shellcomponent/src/cli/mkdirCommand.ts @@ -0,0 +1,40 @@ +import { + registerCliCommand, + rejectUnknownOptions, + type CliCommandDefinition, +} from "@keeper-security/keeper-sdk-javascript"; + +const MKDIR_ALLOWED = new Set(["p", "parents"]); + +export const mkdirShellCommand: CliCommandDefinition = { + name: "mkdir", + order: 900, + description: + "Host mkdir is disabled in keeper-shell (no filesystem). Use `api-base` for an HTTP CLI backend, or SDK folder APIs.", + usage: "mkdir [-p|--parents] [--] ; mkdir --help", + flagOptions: ["-p", "--parents"], + allowedOptions: MKDIR_ALLOWED, + help: { + title: "mkdir — host filesystem directory (disabled in embedded shell)", + synopsis: " mkdir [-p|--parents] [--] RELATIVE_PATH", + description: ` In keeper-shell with in-browser SDK transport, host mkdir is not available (no + sandboxed filesystem). Set api-base to an HTTP CLI backend if you need + this command, or use Keeper vault folder APIs in code.`, + options: ` -p, --parents (remote server only.) + -- End of options.`, + note: " Vault folder operations live on KeeperVault (mkdir, addFolder, …) in the SDK.", + }, + async run(_host, parsed) { + const bad = rejectUnknownOptions(parsed, MKDIR_ALLOWED, "mkdir"); + if (bad) return bad; + return { + code: 1, + out: "", + err: + "mkdir: not available in keeper-shell (no host filesystem). " + + "Set api-base to an HTTP CLI backend for workspace mkdir, or use Keeper vault folder APIs.\n", + }; + }, +}; + +registerCliCommand(mkdirShellCommand); diff --git a/shellcomponent/src/cli/types.ts b/shellcomponent/src/cli/types.ts index c10abf09..abba04bf 100644 --- a/shellcomponent/src/cli/types.ts +++ b/shellcomponent/src/cli/types.ts @@ -1,12 +1 @@ -export type CliResult = { - code: number; - out: string; - err: string; - /** - * Login has username but needs password: UI should call login transport with JSON - * `{ username, password }` — never put the password in `line`. - */ - needPassword?: boolean; - /** When `needPassword`, the username from `login --username …`. */ - loginUsername?: string; -}; +export type { CliResult } from "@keeper-security/keeper-sdk-javascript"; diff --git a/shellcomponent/tsconfig.json b/shellcomponent/tsconfig.json index d071cfd5..c088d75a 100644 --- a/shellcomponent/tsconfig.json +++ b/shellcomponent/tsconfig.json @@ -12,7 +12,7 @@ "types": ["node"], "baseUrl": ".", "paths": { - "@keeper-security/keeper-sdk-javascript": ["../KeeperSdk/src/index.ts"] + "@keeper-security/keeper-sdk-javascript": ["../KeeperSdk/src/browser.ts"] } }, "include": ["src/**/*.ts", "vite.config.ts"] diff --git a/shellcomponent/vite.config.ts b/shellcomponent/vite.config.ts index 2b7ac858..7740d351 100644 --- a/shellcomponent/vite.config.ts +++ b/shellcomponent/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ root: ".", resolve: { alias: { - "@keeper-security/keeper-sdk-javascript": resolve(__dirname, "../KeeperSdk/src/index.ts"), + "@keeper-security/keeper-sdk-javascript": resolve(__dirname, "../KeeperSdk/src/browser.ts"), "fs/promises": resolve(__dirname, "src/shims/fs-promises-empty.ts"), fs: resolve(__dirname, "src/shims/fs-empty.ts"), path: resolve(__dirname, "node_modules/path-browserify/index.js"), From 27793ad9a89da4092cba4f16e42a21ad5af7e444 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Thu, 21 May 2026 09:06:54 +0530 Subject: [PATCH 05/23] added session restore logic --- KeeperSdk/package-lock.json | 14 +- KeeperSdk/package.json | 3 +- KeeperSdk/src/api.ts | 8 + KeeperSdk/src/auth/sessionRestore.ts | 201 +++++++++++++++++ KeeperSdk/src/cli/commands/restoreSession.ts | 212 ++++++++++++++++++ KeeperSdk/src/cli/commands/sync.ts | 63 ++++++ KeeperSdk/src/cli/dispatch.ts | 20 +- KeeperSdk/src/cli/index.ts | 14 ++ KeeperSdk/src/cli/jsonArg.ts | 30 +++ KeeperSdk/src/cli/types.ts | 8 +- KeeperSdk/src/vault/KeeperVault.ts | 27 +++ examples/sdk_example/README.md | 18 +- examples/sdk_example/package.json | 1 + .../sdk_example/src/auth/register_device.ts | 108 ++++----- .../sdk_example/src/auth/restore_session.ts | 32 +++ .../sdk_example/src/records/list_records.ts | 11 +- examples/sdk_example/src/utils/restoreAuth.ts | 113 ++++++++++ shellcomponent/package-lock.json | 14 +- shellcomponent/package.json | 2 + shellcomponent/src/KeeperShell.ts | 54 ++++- shellcomponent/src/cli/cliDispatch.ts | 11 +- shellcomponent/src/cli/keeperCliHost.ts | 52 ++++- shellcomponent/vite.config.ts | 27 ++- 23 files changed, 948 insertions(+), 95 deletions(-) create mode 100644 KeeperSdk/src/auth/sessionRestore.ts create mode 100644 KeeperSdk/src/cli/commands/restoreSession.ts create mode 100644 KeeperSdk/src/cli/commands/sync.ts create mode 100644 KeeperSdk/src/cli/jsonArg.ts create mode 100644 examples/sdk_example/src/auth/restore_session.ts create mode 100644 examples/sdk_example/src/utils/restoreAuth.ts diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 90fbb215..0c2c31f6 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -91,13 +91,13 @@ "link": true }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/asmcrypto.js": { @@ -136,9 +136,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" } diff --git a/KeeperSdk/package.json b/KeeperSdk/package.json index 890857ee..a0dcd9c4 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -13,7 +13,8 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "clean": "rm -rf dist", + "clean": "rm -rf dist node_modules", + "rebuild": "npm run clean && npm install && npm run build", "link-local": "npm link ../keeperapi", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts index ebe75903..7875bf3a 100644 --- a/KeeperSdk/src/api.ts +++ b/KeeperSdk/src/api.ts @@ -84,6 +84,14 @@ export type { export { KeeperVault } from './vault/KeeperVault' export type { KeeperVaultConfig, VaultSummary } from './vault/KeeperVault' +export type { SessionRestoreInput } from './auth/sessionRestore' +export { + toSessionParams, + validateSessionRestoreInput, + sessionRestoreFromJson, + resolveSessionRestorePayload, +} from './auth/sessionRestore' + export { getFolder, findFolder, GetFolderFormat } from './folders/getFolder' export type { GetFolderOptions, diff --git a/KeeperSdk/src/auth/sessionRestore.ts b/KeeperSdk/src/auth/sessionRestore.ts new file mode 100644 index 00000000..0e64a4e0 --- /dev/null +++ b/KeeperSdk/src/auth/sessionRestore.ts @@ -0,0 +1,201 @@ +import { Authentication, normal64Bytes, type SessionParams } from '@keeper-security/keeperapi' +import { KeeperSdkError, ResultCodes } from '../utils' + +type UserType = SessionParams['userType'] + +const UserTypeValues = { + normal: 'normal' as UserType, + cloudSso: 'cloud_sso' as UserType, + onsiteSso: 'onsite_sso' as UserType, +} + +/** String form of {@link SessionParams} as exported from extension / vault storage. */ +export type SessionRestoreInput = { + accountUid: string + clientKey: string + dataKey: string + eccPrivateKey: string + eccPublicKey: string + messageSessionUid: string + privateKey: string + sessionToken: string + sessionTokenType: number | string + username: string + userType: number | string + ssoLogoutUrl?: string + ssoSessionId?: string + enterprisePublicKey?: string + enterpriseEccPublicKey?: string +} + +const REQUIRED_KEYS: (keyof SessionRestoreInput)[] = [ + 'accountUid', + 'clientKey', + 'dataKey', + 'eccPrivateKey', + 'eccPublicKey', + 'messageSessionUid', + 'privateKey', + 'sessionToken', + 'sessionTokenType', + 'username', + 'userType', +] + +function decodeBytes(label: string, value: string | undefined, required: boolean): Uint8Array | undefined { + const trimmed = typeof value === 'string' ? value.trim() : '' + if (!trimmed) { + if (required) { + throw new KeeperSdkError(`restore-session: missing required field: ${label}`, ResultCodes.MISSING_USERNAME) + } + return undefined + } + try { + return normal64Bytes(trimmed) + } catch (e) { + const msg = e instanceof Error ? e.message : String(e) + throw new KeeperSdkError(`restore-session: invalid base64 for ${label}: ${msg}`, ResultCodes.INVALID_CREDENTIALS) + } +} + +function parseUserType(value: number | string): UserType { + if (typeof value === 'number') { + switch (value) { + case 0: + return UserTypeValues.normal + case 1: + return UserTypeValues.cloudSso + case 2: + return UserTypeValues.onsiteSso + default: + break + } + } + const s = String(value).toLowerCase() + if (s === 'normal' || s === '0') return UserTypeValues.normal + if (s === 'cloud_sso' || s === 'cloudsso' || s === '1') return UserTypeValues.cloudSso + if (s === 'onsite_sso' || s === 'onsitesso' || s === '2') return UserTypeValues.onsiteSso + throw new KeeperSdkError(`restore-session: unknown userType: ${value}`, ResultCodes.INVALID_CREDENTIALS) +} + +function parseSessionTokenType(value: number | string): Authentication.SessionTokenType { + const n = typeof value === 'number' ? value : Number(value) + if (!Number.isFinite(n)) { + throw new KeeperSdkError(`restore-session: invalid sessionTokenType: ${value}`, ResultCodes.INVALID_CREDENTIALS) + } + return n as Authentication.SessionTokenType +} + +export function validateSessionRestoreInput(input: Partial): SessionRestoreInput { + const missing = REQUIRED_KEYS.filter((k) => { + const v = input[k] + return v === undefined || v === null || (typeof v === 'string' && !v.trim()) + }) + if (missing.length > 0) { + throw new KeeperSdkError( + `restore-session: missing required field(s): ${missing.join(', ')}`, + ResultCodes.MISSING_USERNAME + ) + } + return input as SessionRestoreInput +} + +/** Build keeperapi {@link SessionParams} from extension-style strings. */ +export function toSessionParams(input: SessionRestoreInput): SessionParams { + const enterprisePublicKey = decodeBytes('enterprisePublicKey', input.enterprisePublicKey, false) + const enterpriseEccPublicKey = decodeBytes('enterpriseEccPublicKey', input.enterpriseEccPublicKey, false) + + return { + accountUid: decodeBytes('accountUid', input.accountUid, true)!, + clientKey: decodeBytes('clientKey', input.clientKey, true)!, + dataKey: decodeBytes('dataKey', input.dataKey, true)!, + eccPrivateKey: decodeBytes('eccPrivateKey', input.eccPrivateKey, true)!, + eccPublicKey: decodeBytes('eccPublicKey', input.eccPublicKey, true)!, + messageSessionUid: decodeBytes('messageSessionUid', input.messageSessionUid, true)!, + privateKey: decodeBytes('privateKey', input.privateKey, true)!, + sessionToken: input.sessionToken.trim(), + sessionTokenType: parseSessionTokenType(input.sessionTokenType), + username: input.username.trim(), + userType: parseUserType(input.userType), + ssoLogoutUrl: input.ssoLogoutUrl?.trim() ?? '', + ssoSessionId: input.ssoSessionId?.trim() ?? '', + ...(enterprisePublicKey ? { enterprisePublicKey } : {}), + ...(enterpriseEccPublicKey ? { enterpriseEccPublicKey } : {}), + } +} + +function assertBodyIsJsonNotHtml(body: string, source: string): void { + const head = body.trimStart().slice(0, 32).toLowerCase() + if (head.startsWith(') +} + +/** Parse `--from-json` payload, or read a file when the value is not JSON. */ +export async function resolveSessionRestorePayload( + raw: string, + readFile?: (path: string) => Promise +): Promise { + const text = raw.trim() + if (readFile && looksLikeFilePath(text)) { + const body = await readFile(text) + assertBodyIsJsonNotHtml(body, text) + return sessionRestoreFromJson(body) + } + try { + return sessionRestoreFromJson(text) + } catch (e) { + if (looksLikeInlineJson(text) || !readFile) { + throw e + } + const body = await readFile(text) + assertBodyIsJsonNotHtml(body, text) + return sessionRestoreFromJson(body) + } +} diff --git a/KeeperSdk/src/cli/commands/restoreSession.ts b/KeeperSdk/src/cli/commands/restoreSession.ts new file mode 100644 index 00000000..cf8976cc --- /dev/null +++ b/KeeperSdk/src/cli/commands/restoreSession.ts @@ -0,0 +1,212 @@ +import type { CliCommandDefinition, KeeperCliHost, ParsedCli } from '../types' +import { + resolveSessionRestorePayload, + validateSessionRestoreInput, + type SessionRestoreInput, +} from '../../auth/sessionRestore' +import { getOpt, hasOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { runVaultSync } from './sync' + +/** Flags that may appear after `--from-json ` on the same line (stripped before parse/read). */ +export const RESTORE_SESSION_TRAILING_OPTS = [ + 'sync', + 'account-uid', + 'client-key', + 'data-key', + 'ecc-private-key', + 'ecc-public-key', + 'message-session-uid', + 'private-key', + 'session-token', + 'st', + 'session-token-type', + 'username', + 'user', + 'user-type', + 'sso-logout-url', + 'sso-session-id', + 'enterprise-public-key', + 'enterprise-ecc-public-key', +] as const + +const RESTORE_ALLOWED = new Set([ + 'sync', + 'from-json', + 'account-uid', + 'client-key', + 'data-key', + 'ecc-private-key', + 'ecc-public-key', + 'message-session-uid', + 'private-key', + 'session-token', + 'st', + 'session-token-type', + 'username', + 'user', + 'user-type', + 'sso-logout-url', + 'sso-session-id', + 'enterprise-public-key', + 'enterprise-ecc-public-key', +]) + +const ENV_PREFIX = 'RESTORE_SESSION_' + +const FIELD_ENV: Record = { + ACCOUNT_UID: 'accountUid', + CLIENT_KEY: 'clientKey', + DATA_KEY: 'dataKey', + ECC_PRIVATE_KEY: 'eccPrivateKey', + ECC_PUBLIC_KEY: 'eccPublicKey', + MESSAGE_SESSION_UID: 'messageSessionUid', + PRIVATE_KEY: 'privateKey', + SESSION_TOKEN: 'sessionToken', + SESSION_TOKEN_TYPE: 'sessionTokenType', + USERNAME: 'username', + USER_TYPE: 'userType', + SSO_LOGOUT_URL: 'ssoLogoutUrl', + SSO_SESSION_ID: 'ssoSessionId', + ENTERPRISE_PUBLIC_KEY: 'enterprisePublicKey', + ENTERPRISE_ECC_PUBLIC_KEY: 'enterpriseEccPublicKey', +} + +const OPT_TO_FIELD: Record = { + 'account-uid': 'accountUid', + 'client-key': 'clientKey', + 'data-key': 'dataKey', + 'ecc-private-key': 'eccPrivateKey', + 'ecc-public-key': 'eccPublicKey', + 'message-session-uid': 'messageSessionUid', + 'private-key': 'privateKey', + 'session-token': 'sessionToken', + st: 'sessionToken', + 'session-token-type': 'sessionTokenType', + username: 'username', + user: 'username', + 'user-type': 'userType', + 'sso-logout-url': 'ssoLogoutUrl', + 'sso-session-id': 'ssoSessionId', + 'enterprise-public-key': 'enterprisePublicKey', + 'enterprise-ecc-public-key': 'enterpriseEccPublicKey', +} + +function envField(host: KeeperCliHost, key: keyof SessionRestoreInput): string | undefined { + const envKey = Object.entries(FIELD_ENV).find(([, v]) => v === key)?.[0] + return envKey ? host.envString(`${ENV_PREFIX}${envKey}`) : undefined +} + +function buildInputFromFlags(host: KeeperCliHost, parsed: ParsedCli): SessionRestoreInput { + const partial: Partial = {} + + for (const [opt, field] of Object.entries(OPT_TO_FIELD)) { + const fromFlag = getOpt(parsed.opts, opt) + if (fromFlag !== undefined) { + ;(partial as Record)[field] = fromFlag + continue + } + const fromEnv = envField(host, field) + if (fromEnv !== undefined) { + ;(partial as Record)[field] = fromEnv + } + } + + return validateSessionRestoreInput(partial) +} + +export const restoreSessionCommand: CliCommandDefinition = { + name: 'restore-session', + order: 12, + description: + 'Restore a logged-in session from extension SessionParams (continueSession; no device keys required).', + usage: + 'restore-session --from-json FILE|JSON [--sync] OR restore-session --session-token … (see --help)', + flagOptions: [ + '--sync', + '--from-json', + '--account-uid', + '--client-key', + '--data-key', + '--ecc-private-key', + '--ecc-public-key', + '--message-session-uid', + '--private-key', + '--session-token', + '--session-token-type', + '--username', + '--user-type', + '--sso-logout-url', + '--sso-session-id', + '--enterprise-public-key', + '--enterprise-ecc-public-key', + ], + allowedOptions: RESTORE_ALLOWED, + help: { + title: 'restore-session — restore SessionParams from extension / vault export', + synopsis: ` restore-session --from-json session.json + restore-session --session-token TOKEN --username U --account-uid B64 …`, + description: ` Loads a full keeperapi SessionParams snapshot and calls Auth.continueSession() + (same path as the browser extension after login). Use this when you have + accountUid, clientKey, dataKey, keys, sessionToken, username, etc. from + extension storage — not deviceToken/device private key. + + Provide parameters either as one JSON object (--from-json) or as flags / env. + Binary fields are base64 or base64url (same as normal64Bytes in keeperapi).`, + options: ` --from-json Inline JSON (object or JSON-stringified object), or a file path + The entire remainder of the command line is passed to JSON.parse (then file read if needed). + --account-uid, --client-key, --data-key, --ecc-private-key, --ecc-public-key + --message-session-uid, --private-key + --session-token, --st Session token string (as stored; often base64url) + --session-token-type Numeric SessionTokenType enum + --username, --user + --user-type 0=normal, 1=cloud_sso, 2=onsite_sso (or string names) + --sso-logout-url, --sso-session-id + --enterprise-public-key, --enterprise-ecc-public-key (optional) + --sync Run syncDown after restoring the session`, + environment: ` RESTORE_SESSION_JSON Same as --from-json + RESTORE_SESSION_ACCOUNT_UID Per-field overrides (see --help flags) + RESTORE_SESSION_SESSION_TOKEN + … (RESTORE_SESSION_ for each field above)`, + keeperSdk: ' KeeperVault.restoreSession(input) → SessionManager.saveSessionParameters + Auth.continueSession', + note: ' sessionToken expires; region must match keeper-host / KEEPER_HOST.', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(restoreSessionCommand), err: '' } + } + const bad = rejectUnknownOptions(parsed, RESTORE_ALLOWED, 'restore-session') + if (bad) return bad + if (parsed.positional.length > 0) { + return { code: 1, out: '', err: 'restore-session: unexpected positional arguments\n' } + } + + try { + let input: SessionRestoreInput + const jsonRaw = getOpt(parsed.opts, 'from-json') ?? host.envString('RESTORE_SESSION_JSON') + if (jsonRaw) { + const readFile = + host.readTextFile ?? + (typeof document === 'undefined' + ? async (path: string) => (await import('fs/promises')).readFile(path, 'utf8') + : undefined) + input = await resolveSessionRestorePayload(jsonRaw, readFile) + } else { + input = buildInputFromFlags(host, parsed) + } + + await host.getVault().restoreSession(input) + let out = `keeper: session restored for ${input.username}.\n` + if (hasOpt(parsed.opts, 'sync')) { + const syncResult = await runVaultSync(host) + if (syncResult.code !== 0) { + return syncResult + } + out += syncResult.out + } + return { code: 0, out, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError('restore-session', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/sync.ts b/KeeperSdk/src/cli/commands/sync.ts new file mode 100644 index 00000000..0f0d91b8 --- /dev/null +++ b/KeeperSdk/src/cli/commands/sync.ts @@ -0,0 +1,63 @@ +import type { SyncResult } from '@keeper-security/keeperapi' +import type { CliCommandDefinition, CliResult, KeeperCliHost } from '../types' +import { wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureLoggedIn } from './login' + +function formatSyncSummary(result: SyncResult): string { + const lines = [`keeper: sync complete for ${result.username}.`, ` pages: ${result.pageCount}`] + if (result.totalTime) lines.push(` total: ${result.totalTime}`) + if (result.networkTime) lines.push(` network: ${result.networkTime}`) + const counts = result.counts ?? {} + const parts = Object.entries(counts) + .filter(([, n]) => typeof n === 'number' && n > 0) + .map(([k, n]) => `${k}=${n}`) + if (parts.length) lines.push(` counts: ${parts.join(', ')}`) + if (result.error) lines.push(` warning: ${result.error}`) + return lines.join('\n') + '\n' +} + +/** Download vault data via keeperapi syncDown (KeeperVault.sync). */ +export async function runVaultSync(host: KeeperCliHost): Promise { + const v = host.getVault() + if (!v.isLoggedIn) { + const login = await ensureLoggedIn(host) + if (login.code !== 0) return login + } + const result = await v.sync() + return { code: 0, out: formatSyncSummary(result), err: '' } +} + +export const syncCommand: CliCommandDefinition = { + name: 'sync', + order: 20, + aliases: ['syncdown'], + description: 'Download / refresh vault data from Keeper (syncDown).', + usage: 'sync [--help|-h]', + help: { + title: 'sync — download vault data (syncDown)', + synopsis: ' sync', + description: ` Calls KeeperVault.sync() → keeperapi syncDown to pull records, folders, and + related vault data into local storage. Requires an active session (login or restore-session).`, + options: ' --help, -h Show this help.', + keeperSdk: ' KeeperVault.sync() → syncDown({ auth, storage })', + seeAlso: ' restore-session --sync, records list, folders list', + appendVaultSurface: true, + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(syncCommand), err: '' } + } + if (parsed.opts.size > 0) { + return { code: 1, out: '', err: 'sync: unknown option (try: sync --help)\n' } + } + if (parsed.positional.length > 0) { + return { code: 1, out: '', err: 'sync: unexpected arguments (try: sync --help)\n' } + } + try { + return await runVaultSync(host) + } catch (e) { + return { code: 1, out: '', err: host.formatError('sync', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/dispatch.ts b/KeeperSdk/src/cli/dispatch.ts index 0b6e0175..17b12133 100644 --- a/KeeperSdk/src/cli/dispatch.ts +++ b/KeeperSdk/src/cli/dispatch.ts @@ -1,18 +1,21 @@ -import type { CliResult, KeeperCliHost } from './types' +import type { CliResult, KeeperCliHost, ParsedCli } from './types' import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' +import { extractFromJsonFlagValue } from './jsonArg' +import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' import { formatDetailedHelpForCommand } from './help' import { getCliCommand } from './registry' export async function dispatchKeeperCli( commandName: string, args: string[], - host: KeeperCliHost + host: KeeperCliHost, + preParsed?: ParsedCli ): Promise { const def = getCliCommand(commandName) if (!def) { return { code: 1, out: '', err: `Unknown command: ${commandName}\n` } } - const parsed = parseCliArgs(args) + const parsed = preParsed ?? parseCliArgs(args) if (wantsCliHelp(parsed)) { return { code: 0, out: formatDetailedHelpForCommand(def), err: '' } } @@ -29,5 +32,14 @@ export async function dispatchCliLine(line: string, host: KeeperCliHost): Promis if (!name) { return { code: 0, out: '', err: '' } } - return dispatchKeeperCli(name, tokens.slice(1), host) + const args = tokens.slice(1) + let preParsed: ParsedCli | undefined + if (name === 'restore-session') { + const json = extractFromJsonFlagValue(trimmed, 'from-json', RESTORE_SESSION_TRAILING_OPTS) + if (json) { + preParsed = parseCliArgs(args) + preParsed.opts.set('from-json', json) + } + } + return dispatchKeeperCli(name, args, host, preParsed) } diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts index a6bd67bc..a7ca6fff 100644 --- a/KeeperSdk/src/cli/index.ts +++ b/KeeperSdk/src/cli/index.ts @@ -5,6 +5,8 @@ import { loginCommand } from './commands/login' import { logoutCommand } from './commands/logout' import { recordsCommand } from './commands/records' import { registerDeviceCommand } from './commands/registerDevice' +import { restoreSessionCommand } from './commands/restoreSession' +import { syncCommand } from './commands/sync' let registryInitialized = false @@ -15,6 +17,8 @@ export function ensureKeeperCliRegistry(): void { registerCliCommand(helpCommand) registerCliCommand(loginCommand) registerCliCommand(registerDeviceCommand) + registerCliCommand(restoreSessionCommand) + registerCliCommand(syncCommand) registerCliCommand(recordsCommand) registerCliCommand(foldersCommand) registerCliCommand(logoutCommand) @@ -82,5 +86,15 @@ export { recordsCommand } from './commands/records' export { foldersCommand } from './commands/folders' export { registerDeviceCommand } from './commands/registerDevice' export { helpCommand } from './commands/help' +export { restoreSessionCommand } from './commands/restoreSession' +export { syncCommand, runVaultSync } from './commands/sync' export { utf8ToBase64Url, recordUid } from './utils' + +export type { SessionRestoreInput } from '../auth/sessionRestore' +export { + toSessionParams, + validateSessionRestoreInput, + sessionRestoreFromJson, + resolveSessionRestorePayload, +} from '../auth/sessionRestore' diff --git a/KeeperSdk/src/cli/jsonArg.ts b/KeeperSdk/src/cli/jsonArg.ts new file mode 100644 index 00000000..6a11c1bc --- /dev/null +++ b/KeeperSdk/src/cli/jsonArg.ts @@ -0,0 +1,30 @@ +/** + * Everything after `--from-json` on the command line (trimmed). No tokenization — callers use JSON.parse. + * Trailing flags such as `--sync` are stripped via {@link stripTrailingCliFlags}. + */ +export function extractFromJsonFlagValue( + line: string, + flag = 'from-json', + trailingFlags: readonly string[] = [] +): string | null { + const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const flagRe = new RegExp(`(?:^|\\s)--${escaped}(?:\\s*=\\s*|\\s+)`, 'i') + const m = line.match(flagRe) + if (!m || m.index === undefined) return null + const rest = line.slice(m.index + m[0].length).trim() + if (!rest) return null + return stripTrailingCliFlags(rest, trailingFlags) +} + +/** Remove trailing ` --flag` tokens (e.g. `--sync` after a file path or JSON blob). */ +export function stripTrailingCliFlags(value: string, flagNames: readonly string[]): string { + if (flagNames.length === 0) return value.trim() + let s = value.trim() + const parts = flagNames.map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + const alt = parts.join('|') + const tailFlag = new RegExp(`\\s+--(?:${alt})(?:\\s*=\\s*(?:"[^"]*"|\\S+))?\\s*$`, 'i') + while (tailFlag.test(s)) { + s = s.replace(tailFlag, '').trim() + } + return s +} diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts index 8b175282..923afaae 100644 --- a/KeeperSdk/src/cli/types.ts +++ b/KeeperSdk/src/cli/types.ts @@ -1,4 +1,5 @@ -import type { DRecord, DSharedFolder } from '@keeper-security/keeperapi' +import type { DRecord, DSharedFolder, SyncResult } from '@keeper-security/keeperapi' +import type { SessionRestoreInput } from '../auth/sessionRestore' export type CliResult = { code: number @@ -20,10 +21,11 @@ export type KeeperCliVault = { login(username: string, password: string): Promise loginWithSessionToken(username: string, sessionToken: string): Promise logout(): Promise - sync(): Promise + sync(): Promise getRecords(): DRecord[] getSharedFolders(): DSharedFolder[] registerDevice(deviceToken: string, privateKey: string, options?: { username?: string }): Promise + restoreSession(input: SessionRestoreInput): Promise } /** Host adapter (browser shell, Node Commander, tests). */ @@ -31,6 +33,8 @@ export type KeeperCliHost = { getVault(): KeeperCliVault envString(name: string): string | undefined formatError(context: string, err: unknown): string + /** Read a local or remote text file (browser dev: Vite `/@fs/…` paths). */ + readTextFile?: (path: string) => Promise } export type CliHelpDoc = { diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index c5424897..44859016 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -14,6 +14,10 @@ import type { SyncResult, SyncLogFormat, VaultStorage, SessionStorage, AuthUI3 } import { InMemoryStorage } from '../storage/InMemoryStorage' import { SessionManager } from '../auth/SessionManager' import { getSdkPlatform } from '../platform' +import { + toSessionParams, + type SessionRestoreInput, +} from '../auth/sessionRestore' import { searchRecords, formatRecord, getRecordTitle, getRecordType } from '../records/RecordUtils' import { addRecord as addRecordOp, @@ -292,6 +296,29 @@ export class KeeperVault { return this.auth?.sessionToken || undefined } + /** + * Restore an existing Keeper session from extension-style {@link SessionRestoreInput} + * (maps to keeperapi `SessionParams` + `continueSession`). Does not run `loginV3` or require device keys. + */ + public async restoreSession(input: SessionRestoreInput): Promise { + const params = toSessionParams(input) + await this.sessionManager.saveSessionParameters(params) + this.sessionManager.setLastUsername(params.username) + + this.auth = await this.createAuth() + await this.auth.continueSession() + + if (!this.auth.sessionToken) { + throw new KeeperSdkError( + 'Session restore failed — session token may be expired or invalid.', + ResultCodes.SESSION_TOKEN_EXPIRED + ) + } + + this.synced = false + this.log.info(`Session restored for ${params.username}`) + } + public async resumeSession(): Promise { const username = await this.sessionManager.getLastUsername() if (!username) { diff --git a/examples/sdk_example/README.md b/examples/sdk_example/README.md index 7a7477c3..dd54fb67 100644 --- a/examples/sdk_example/README.md +++ b/examples/sdk_example/README.md @@ -24,20 +24,24 @@ npm run link-local Examples use `~/.keeper/config.json` for saved credentials and persistent login. If the file is not found, you will be prompted for server, username, and password. -## Available Examples +For restore-session flows, provide a path to session JSON (extension `SessionParams` shape). There is no default path. + + ## Available Examples ### Authentication | Command | Description | |---|---| | `npm run auth:login` | Master password login with retry logic, masked input, and vault sync. Automatically attempts persistent login (via clone code from `~/.keeper/config.json`) before falling back to the password prompt. | -| `npm run auth:session-token` | Login using an existing session token for pre-authenticated workflows. Prompts for username, host, and session token. | +| `npm run auth:session-token` | Login using an existing session token for pre-authenticated workflows. Prompts for username, host, and session token. Requires device token + private key in `~/.keeper/config.json` or use `auth:register-device` first in the same run. | +| `npm run auth:register-device` | Store device token + device private key on a `KeeperVault` (in-memory), optionally then `loginWithSessionToken` + sync. | +| `npm run auth:restore-session` | Restore extension-style session JSON (prompts for file path; required), then syncDown. No device keys or `loginV3`. | ### Records | Command | Description | |---|---| -| `npm run records:list` | List all records in the vault | +| `npm run records:list` | List all records in the vault (password / persistent login via `login()`) | | `npm run records:get` | Get details of a specific record by UID or title | | `npm run records:add` | Add a new typed record to the vault | | `npm run records:update` | Update fields on an existing record | @@ -63,3 +67,11 @@ npm run records:get ``` Most examples will log in automatically using persistent login (if configured) or prompt for credentials. After authentication, follow the interactive prompts. + +**Restore-session flag** on `records:list` (requires `--from-json`; no default path): + +```bash +npm run records:list -- --restore-session --from-json /path/to/session.json +npm run records:list -- --restore-session --from-json /path/to/session.json --host keepersecurity.eu +npm run records:list -- --restore-session --from-json /path/to/session.json --no-sync +``` diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index f5457fcc..9d614a22 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -6,6 +6,7 @@ "auth:login": "ts-node src/auth/login.ts", "auth:session-token": "ts-node src/auth/session_token_login.ts", "auth:register-device": "ts-node src/auth/register_device.ts", + "auth:restore-session": "ts-node src/auth/restore_session.ts", "records:list": "ts-node src/records/list_records.ts", "records:get": "ts-node src/records/get_record.ts", "records:add": "ts-node src/records/add_record.ts", diff --git a/examples/sdk_example/src/auth/register_device.ts b/examples/sdk_example/src/auth/register_device.ts index 602ef4f4..2952be9f 100644 --- a/examples/sdk_example/src/auth/register_device.ts +++ b/examples/sdk_example/src/auth/register_device.ts @@ -1,70 +1,76 @@ import { KeeperVault, - SessionManager, KeeperSdkError, - loadKeeperConfig, - resolveServer, prompt, suppressLogs, cleanup, logger, - extractResultCode, SdkDefaults, ResultCodes, } from '@keeper-security/keeper-sdk-javascript' import { runExample } from '../utils/runner' -async function registerDevice() { - const config = await loadKeeperConfig() - const defaultUsername = config.last_login || config.user || '' - let username: string - if (defaultUsername) { - logger.info(`Using saved username: ${defaultUsername}`) - username = defaultUsername - } else { - username = await prompt('Username (email): ') - if (!username) throw new KeeperSdkError('Username is required.', ResultCodes.MISSING_USERNAME) + +/** + * Store device token + device private key on a KeeperVault (in-memory for this process). + * Use before loginWithSessionToken when ~/.keeper/config.json has no device yet. + */ +async function main() { + const username = await prompt('Username (email): ') + if (!username) throw new KeeperSdkError('Username is required.', ResultCodes.MISSING_USERNAME) + + const host = (await prompt('Host [keepersecurity.com]: ')).trim() || 'keepersecurity.com' + const deviceToken = await prompt('Device token (base64 / base64url): ') + if (!deviceToken.trim()) { + throw new KeeperSdkError('Device token is required.', ResultCodes.INVALID_CREDENTIALS) } - const host = await resolveServer(username) - logger.info(`Host resolved to: ${host}`) - const sessionManager = new SessionManager() - const vault = new KeeperVault({ - host, - clientVersion: SdkDefaults.CLIENT_VERSION, - sessionStorage: sessionManager, - }) + const privateKey = await prompt('Device private key (base64 / base64url): ') + if (!privateKey.trim()) { + throw new KeeperSdkError('Device private key is required.', ResultCodes.INVALID_CREDENTIALS) + } + + const vault = new KeeperVault({ host, clientVersion: SdkDefaults.CLIENT_VERSION }) + try { - const MAX_ATTEMPTS = 5 - for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - const password = await prompt('Password: ', true) - if (!password) throw new KeeperSdkError('Password is required.', ResultCodes.MISSING_PASSWORD) - const restore = suppressLogs() - try { - await vault.login(username, password) - restore() - break - } catch (err) { - restore() - const resultCode = extractResultCode(err) - if (resultCode === ResultCodes.INVALID_CREDENTIALS) { - const remaining = MAX_ATTEMPTS - attempt - if (remaining > 0) { - logger.warn(`Incorrect password (${remaining} attempt(s) remaining)`) - continue - } - throw new KeeperSdkError('Maximum attempts exceeded.', ResultCodes.MAX_ATTEMPTS_EXCEEDED) - } - throw err - } + await vault.registerDevice(deviceToken.trim(), privateKey.trim(), { username }) + logger.info(`Device credentials stored in memory for ${host} (this Node process).`) + logger.info('They are not written to ~/.keeper/config.json unless you use a full password login.\n') + + const tryLogin = (await prompt('Login with session token now? [y/N]: ')).trim().toLowerCase() + if (tryLogin !== 'y' && tryLogin !== 'yes') { + logger.info('Done. Call loginWithSessionToken on the same vault instance, or run auth:session-token.') + return + } + + const sessionToken = await prompt('Session token: ') + if (!sessionToken.trim()) { + throw new KeeperSdkError('Session token is required.', 'missing_session_token') + } + + logger.info(`\nLogging in as ${username} on ${host}...`) + const restore = suppressLogs() + try { + await vault.loginWithSessionToken(username, sessionToken.trim()) + } finally { + restore() + } + + logger.info('Syncing vault...') + const restoreSync = suppressLogs() + try { + await vault.sync() + } finally { + restoreSync() } + + const summary = vault.getSummary() const auth = vault.getAuth() - logger.info('\n--- Session Info ---') - logger.info(` Username: ${auth.username}`) - logger.info(` Server: ${vault.host}`) - logger.info(` Session Token: ${auth.sessionToken ? '(active)' : '(none)'}`) - logger.info(` Data Key: ${auth.dataKey ? '(loaded)' : '(not loaded)'}`) - logger.info('\nDevice registration complete.') + logger.info('--- Session Info ---') + logger.info(` Username: ${auth.username}`) + logger.info(` Records: ${summary.recordCount}`) + logger.info('\nLogin + sync successful.') } finally { cleanup(vault) } } -runExample(registerDevice) \ No newline at end of file + +runExample(main) diff --git a/examples/sdk_example/src/auth/restore_session.ts b/examples/sdk_example/src/auth/restore_session.ts new file mode 100644 index 00000000..e2dc3804 --- /dev/null +++ b/examples/sdk_example/src/auth/restore_session.ts @@ -0,0 +1,32 @@ +import { prompt, cleanup, logger } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { loginViaRestoreSession, requireSessionJsonPath } from '../utils/restoreAuth' + +async function main() { + const jsonPathInput = await prompt('Session JSON file path (required): ') + const jsonPath = requireSessionJsonPath(jsonPathInput, 'auth:restore-session') + + const host = (await prompt('Host [keepersecurity.com]: ')).trim() || 'keepersecurity.com' + const syncAnswer = (await prompt('Run syncDown after restore? [Y/n]: ')).trim().toLowerCase() + const runSync = syncAnswer !== 'n' && syncAnswer !== 'no' + + const vault = await loginViaRestoreSession({ + jsonPath, + host, + sync: runSync, + }) + + try { + const summary = vault.getSummary() + logger.info('--- Vault summary ---') + logger.info(` Records: ${summary.recordCount}`) + logger.info(` Shared folders: ${summary.sharedFolderCount}`) + logger.info(` Teams: ${summary.teamCount}`) + logger.info(` Folders: ${summary.folderCount}`) + logger.info('\nRestore complete.') + } finally { + cleanup(vault) + } +} + +runExample(main) diff --git a/examples/sdk_example/src/records/list_records.ts b/examples/sdk_example/src/records/list_records.ts index 269092b2..20359d1b 100644 --- a/examples/sdk_example/src/records/list_records.ts +++ b/examples/sdk_example/src/records/list_records.ts @@ -1,8 +1,17 @@ import { login, cleanup, formatRecord, logger } from '@keeper-security/keeper-sdk-javascript' import { runExample } from '../utils/runner' +import { assertRestoreCliArgs, loginViaRestoreSession, parseRestoreCliArgs } from '../utils/restoreAuth' async function listRecords() { - const vault = await login() + const cli = parseRestoreCliArgs() + assertRestoreCliArgs(cli) + const vault = cli.restoreSession + ? await loginViaRestoreSession({ + jsonPath: cli.jsonPath, + host: cli.host, + sync: !cli.noSync, + }) + : await login() try { const records = vault.getRecords() diff --git a/examples/sdk_example/src/utils/restoreAuth.ts b/examples/sdk_example/src/utils/restoreAuth.ts new file mode 100644 index 00000000..454beb2e --- /dev/null +++ b/examples/sdk_example/src/utils/restoreAuth.ts @@ -0,0 +1,113 @@ +import * as fs from 'fs/promises' +import * as path from 'path' +import { + KeeperVault, + KeeperSdkError, + logger, + SdkDefaults, + suppressLogs, + resolveSessionRestorePayload, + ResultCodes, +} from '@keeper-security/keeper-sdk-javascript' + +export type RestoreSessionLoginOptions = { + /** Path to extension-style session JSON (required). */ + jsonPath: string + host?: string + /** Run syncDown after restore (default true). */ + sync?: boolean +} + +export function requireSessionJsonPath(jsonPath: string | undefined, context: string): string { + const trimmed = jsonPath?.trim() + if (!trimmed) { + throw new KeeperSdkError( + `${context}: session JSON path is required (no default). ` + + 'Pass --from-json /path/to/session.json or set RESTORE_SESSION_JSON.', + ResultCodes.INVALID_CREDENTIALS + ) + } + return path.resolve(trimmed.replace(/^~/, process.env.HOME || '')) +} + +/** + * Authenticate via SessionParams JSON + continueSession (no password / device login). + */ +export async function loginViaRestoreSession(options: RestoreSessionLoginOptions): Promise { + const jsonPath = requireSessionJsonPath(options.jsonPath, 'restore-session') + + try { + await fs.access(jsonPath) + } catch { + throw new KeeperSdkError(`Session JSON file not found: ${jsonPath}`, ResultCodes.INVALID_CREDENTIALS) + } + + const host = options.host?.trim() || 'keepersecurity.com' + const runSync = options.sync !== false + + logger.info(`Restoring session from ${jsonPath} (${host})...`) + + const vault = new KeeperVault({ host, clientVersion: SdkDefaults.CLIENT_VERSION }) + const input = await resolveSessionRestorePayload(jsonPath, (p) => fs.readFile(p, 'utf8')) + + const restore = suppressLogs() + try { + await vault.restoreSession(input) + } finally { + restore() + } + + logger.info(`Authenticated as ${input.username} (restore-session).`) + + if (runSync) { + logger.info('Syncing vault...') + const restoreSync = suppressLogs() + try { + const result = await vault.sync() + logger.info( + `Vault synced (${result.pageCount} page${result.pageCount === 1 ? '' : 's'}, ${vault.getSummary().recordCount} records).\n` + ) + } finally { + restoreSync() + } + } + + return vault +} + +export type RestoreCliArgs = { + restoreSession: boolean + jsonPath?: string + host?: string + noSync: boolean +} + +/** Parse `--restore-session`, `--from-json`, `--host`, `--no-sync` from process.argv. */ +export function parseRestoreCliArgs(argv: string[] = process.argv): RestoreCliArgs { + let restoreSession = false + let jsonPath: string | undefined + let host: string | undefined + let noSync = false + + for (let i = 2; i < argv.length; i++) { + const arg = argv[i] + if (arg === '--restore-session') { + restoreSession = true + } else if (arg === '--from-json' && argv[i + 1]) { + restoreSession = true + jsonPath = argv[++i] + } else if (arg === '--host' && argv[i + 1]) { + host = argv[++i] + } else if (arg === '--no-sync') { + noSync = true + } + } + + return { restoreSession, jsonPath, host, noSync } +} + +/** Validate CLI restore flags before loginViaRestoreSession. */ +export function assertRestoreCliArgs(cli: RestoreCliArgs): asserts cli is RestoreCliArgs & { jsonPath: string } { + if (!cli.restoreSession) return + requireSessionJsonPath(cli.jsonPath, 'records') +} diff --git a/shellcomponent/package-lock.json b/shellcomponent/package-lock.json index a100bf29..240fd6d6 100644 --- a/shellcomponent/package-lock.json +++ b/shellcomponent/package-lock.json @@ -1,12 +1,12 @@ { "name": "@keeper-security/keeper-shell-component", - "version": "1.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@keeper-security/keeper-shell-component", - "version": "1.0.1", + "version": "1.0.0", "license": "ISC", "dependencies": { "@keeper-security/keeper-sdk-javascript": "file:../KeeperSdk", @@ -55,7 +55,7 @@ }, "../KeeperSdk": { "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.0.1", + "version": "1.0.0", "dependencies": { "@keeper-security/keeperapi": "file:../keeperapi", "asmcrypto.js": "^2.3.2", @@ -1156,9 +1156,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -1176,7 +1176,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/shellcomponent/package.json b/shellcomponent/package.json index 7680bbd8..ccca00d7 100644 --- a/shellcomponent/package.json +++ b/shellcomponent/package.json @@ -18,6 +18,8 @@ "keeper-shell.d.ts" ], "scripts": { + "clean": "rm -rf dist node_modules", + "rebuild": "npm run clean && npm install && npm --prefix ../KeeperSdk run rebuild && npm run build", "build:local-keeper-sdk": "npm --prefix ../KeeperSdk install && npm --prefix ../KeeperSdk run build", "dev": "npm install && vite", "dev:inspect": "npm install && node --inspect=9229 ./node_modules/vite/bin/vite.js", diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts index 899a7173..e663a943 100644 --- a/shellcomponent/src/KeeperShell.ts +++ b/shellcomponent/src/KeeperShell.ts @@ -100,6 +100,15 @@ function longestCommonPrefix(values: string[]): string { return pref; } +const PROMPT_PREFIX = "$ "; + +/** Screen rows used by prompt + input when the line wraps (cols from xterm). */ +function promptDisplayRows(displayChars: number, cols: number): number { + const width = Math.max(cols, 1); + if (displayChars <= 0) return 1; + return Math.ceil(displayChars / width); +} + /** Parsed stdin tokens from xterm (arrow keys arrive as ESC sequences). */ type InputTok = | { k: "c"; v: string } @@ -694,6 +703,8 @@ export class KeeperShell extends HTMLElement { let histFromEnd = -1; let histDraft = ""; let cursorPos = 0; + /** Rows on screen for the current prompt line (increases when input wraps). */ + let promptRows = 1; let maskSensitive = this.hasAttribute(ATTR_MASK_INPUT); let pendingLoginUsername: string | null = null; @@ -709,16 +720,27 @@ export class KeeperShell extends HTMLElement { }; const writeFreshPrompt = (): void => { - term.write("$ "); + promptRows = 1; + term.write(PROMPT_PREFIX); afterNewPrompt(); }; + const clearPromptRows = (): void => { + term.write("\r\x1b[2K"); + for (let r = 1; r < promptRows; r++) { + term.write("\x1b[1A\r\x1b[2K"); + } + }; + const writePromptLine = (): void => { const line = this._lineBuf; if (cursorPos < 0) cursorPos = 0; if (cursorPos > line.length) cursorPos = line.length; const visible = maskDisplayActive() ? "*".repeat(line.length) : line; - term.write(`\r\x1b[2K$ ${visible}`); + const displayChars = PROMPT_PREFIX.length + visible.length; + clearPromptRows(); + term.write(`${PROMPT_PREFIX}${visible}`); + promptRows = promptDisplayRows(displayChars, term.cols); const back = line.length - cursorPos; if (back > 0) term.write(`\x1b[${back}D`); }; @@ -952,12 +974,21 @@ export class KeeperShell extends HTMLElement { const handleDataChunk = async (data: string): Promise => { const tokens = feedInput(data, inputCarry); + let redrawPrompt = false; + const flushRedraw = (): void => { + if (redrawPrompt) { + writePromptLine(); + redrawPrompt = false; + } + }; for (const tok of tokens) { if (tok.k === "up") { + flushRedraw(); historyOlder(); continue; } if (tok.k === "down") { + flushRedraw(); historyNewer(); continue; } @@ -965,8 +996,9 @@ export class KeeperShell extends HTMLElement { bumpEditing(); if (cursorPos > 0) { cursorPos--; - writePromptLine(); + redrawPrompt = true; } else { + flushRedraw(); term.write("\x07"); } continue; @@ -975,8 +1007,9 @@ export class KeeperShell extends HTMLElement { bumpEditing(); if (cursorPos < this._lineBuf.length) { cursorPos++; - writePromptLine(); + redrawPrompt = true; } else { + flushRedraw(); term.write("\x07"); } continue; @@ -986,15 +1019,18 @@ export class KeeperShell extends HTMLElement { if (cursorPos < this._lineBuf.length) { const line = this._lineBuf; this._lineBuf = line.slice(0, cursorPos) + line.slice(cursorPos + 1); - writePromptLine(); + redrawPrompt = true; } else { + flushRedraw(); term.write("\x07"); } continue; } const ch = tok.v; if (ch === "\r" || ch === "\n") { + redrawPrompt = false; term.write("\r\n"); + promptRows = 1; await flushLine(); continue; } @@ -1004,15 +1040,17 @@ export class KeeperShell extends HTMLElement { const line = this._lineBuf; this._lineBuf = line.slice(0, cursorPos - 1) + line.slice(cursorPos); cursorPos--; - writePromptLine(); + redrawPrompt = true; } continue; } if (ch === "\t") { + flushRedraw(); await runTabComplete(); continue; } if (ch === "\x0f") { + flushRedraw(); if (pendingLoginUsername === null) { maskSensitive = !maskSensitive; } @@ -1020,6 +1058,7 @@ export class KeeperShell extends HTMLElement { continue; } if (ch === "\x03") { + redrawPrompt = false; this._lineBuf = ""; cursorPos = 0; pendingLoginUsername = null; @@ -1035,8 +1074,9 @@ export class KeeperShell extends HTMLElement { const line = this._lineBuf; this._lineBuf = line.slice(0, cursorPos) + ch + line.slice(cursorPos); cursorPos++; - writePromptLine(); + redrawPrompt = true; } + flushRedraw(); }; term.onData((data) => { diff --git a/shellcomponent/src/cli/cliDispatch.ts b/shellcomponent/src/cli/cliDispatch.ts index 58a25bf3..3dfa954a 100644 --- a/shellcomponent/src/cli/cliDispatch.ts +++ b/shellcomponent/src/cli/cliDispatch.ts @@ -1,5 +1,5 @@ import "./mkdirCommand.js"; -import { dispatchKeeperCli, tokenizeArguments } from "@keeper-security/keeper-sdk-javascript"; +import { dispatchCliLine as sdkDispatchCliLine } from "@keeper-security/keeper-sdk-javascript"; import { shellKeeperCliHost } from "./keeperCliHost.js"; import type { CliResult } from "./types.js"; @@ -9,16 +9,11 @@ const rawMax = : NaN; const MAX_LINE_LENGTH = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 8192; +/** Shell entry: line-length guard, then SDK dispatch (restore-session uses raw `--from-json` tail + JSON.parse). */ export async function dispatchCliLine(line: string): Promise { if (line.length > MAX_LINE_LENGTH) { return { code: 1, out: "", err: "line too long\n" }; } - const tokens = tokenizeArguments(line.trim()); - const name = tokens[0]?.toLowerCase(); - if (!name) { - return { code: 0, out: "", err: "" }; - } - - return dispatchKeeperCli(name, tokens.slice(1), shellKeeperCliHost); + return sdkDispatchCliLine(line, shellKeeperCliHost); } diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts index 89b68c4a..05eca4aa 100644 --- a/shellcomponent/src/cli/keeperCliHost.ts +++ b/shellcomponent/src/cli/keeperCliHost.ts @@ -82,17 +82,63 @@ function asCliVault(v: VaultInstance): KeeperCliVault { login: (u, p) => v.login(u, p), loginWithSessionToken: (u, t) => v.loginWithSessionToken(u, t), logout: () => v.logout(), - sync: async () => { - await v.sync(); - }, + sync: () => v.sync(), getRecords: () => v.getRecords(), getSharedFolders: () => v.getSharedFolders(), registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), + restoreSession: (input) => v.restoreSession(input), + }; +} + +async function readTextFile(path: string): Promise { + const p = path.trim().replace(/^@/, ""); + if (/^https?:\/\//i.test(p)) { + const res = await fetch(p); + if (!res.ok) { + throw new Error(`HTTP ${res.status} loading ${p}`); + } + return res.text(); + } + + const tryFetch = async (url: string): Promise => { + const res = await fetch(url); + if (!res.ok) return null; + const ct = res.headers.get("content-type") ?? ""; + if (ct.includes("text/html")) return null; + const text = await res.text(); + const head = text.trimStart().slice(0, 32).toLowerCase(); + if (head.startsWith(" asCliVault(getVault()), envString, formatError: formatKeeperClientError, + readTextFile, }; diff --git a/shellcomponent/vite.config.ts b/shellcomponent/vite.config.ts index 7740d351..99d8536c 100644 --- a/shellcomponent/vite.config.ts +++ b/shellcomponent/vite.config.ts @@ -1,11 +1,36 @@ -import { defineConfig } from "vite"; +import { readFileSync, existsSync } from "node:fs"; +import { defineConfig, type Plugin } from "vite"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const repoConfJson = resolve(__dirname, "../conf.json"); + +/** Dev-only: load repo-root conf.json without typing a full /@fs path in the shell. */ +function keeperDevSessionConfPlugin(): Plugin { + return { + name: "keeper-dev-session-conf", + configureServer(server) { + server.middlewares.use("/dev/keeper-session.json", (_req, res, next) => { + try { + if (!existsSync(repoConfJson)) { + res.statusCode = 404; + res.end("conf.json not found at repo root"); + return; + } + res.setHeader("Content-Type", "application/json"); + res.end(readFileSync(repoConfJson)); + } catch (e) { + next(e); + } + }); + }, + }; +} export default defineConfig({ root: ".", + plugins: [keeperDevSessionConfPlugin()], resolve: { alias: { "@keeper-security/keeper-sdk-javascript": resolve(__dirname, "../KeeperSdk/src/browser.ts"), From ea5df0f2bbfd7a9f6537487d53dca88218fa3ff7 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Thu, 21 May 2026 13:17:25 +0530 Subject: [PATCH 06/23] added a reverse proxied working copy --- KeeperSdk/src/utils/constants.ts | 9 ++ KeeperSdk/src/vault/KeeperVault.ts | 24 +++-- examples/sdk_example/README.md | 13 ++- examples/sdk_example/package.json | 1 + .../src/records/list_records_shell_cli.ts | 36 +++++++ .../src/utils/inMemoryConfigLoader.ts | 16 +++ examples/sdk_example/src/utils/restoreAuth.ts | 57 ++-------- .../sdk_example/src/utils/shellCliHost.ts | 100 ++++++++++++++++++ .../sdk_example/src/utils/shellCliRestore.ts | 83 +++++++++++++++ shellcomponent/README.md | 22 ++++ shellcomponent/index.html | 1 + shellcomponent/package-lock.json | 64 ++++++++++- shellcomponent/package.json | 2 + shellcomponent/src/dev-bootstrap.ts | 3 + .../src/dev/installKeeperSameOriginProxy.ts | 69 ++++++++++++ shellcomponent/src/dev/keeperProxyHosts.ts | 20 ++++ shellcomponent/vite.config.ts | 3 +- .../vite/keeperSameOriginProxyPlugin.ts | 57 ++++++++++ 18 files changed, 519 insertions(+), 61 deletions(-) create mode 100644 examples/sdk_example/src/records/list_records_shell_cli.ts create mode 100644 examples/sdk_example/src/utils/inMemoryConfigLoader.ts create mode 100644 examples/sdk_example/src/utils/shellCliHost.ts create mode 100644 examples/sdk_example/src/utils/shellCliRestore.ts create mode 100644 shellcomponent/src/dev/installKeeperSameOriginProxy.ts create mode 100644 shellcomponent/src/dev/keeperProxyHosts.ts create mode 100644 shellcomponent/vite/keeperSameOriginProxyPlugin.ts diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index 24d29f8b..af1d9c7c 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -89,6 +89,14 @@ export enum UserErrorCode { TeamUserRemoveFailed = 'team_user_remove_failed', } +export enum SyncErrorCode { + SyncFailed = 'sync_failed', +} + +export enum SyncErrorCode { + SyncFailed = 'sync_failed', +} + export const ResultCodes = { INVALID_CREDENTIALS: AuthErrorCode.InvalidCredentials, MISSING_USERNAME: AuthErrorCode.MissingUsername, @@ -149,6 +157,7 @@ export const ResultCodes = { NO_TEAMS_FOR_USER_OP: UserErrorCode.NoTeamsForUserOp, TEAM_USER_ADD_FAILED: UserErrorCode.TeamUserAddFailed, TEAM_USER_REMOVE_FAILED: UserErrorCode.TeamUserRemoveFailed, + SYNC_FAILED: SyncErrorCode.SyncFailed, } as const export const KEEPER_PUBLIC_HOSTS: Record = { diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 44859016..4a886277 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -367,14 +367,22 @@ export class KeeperVault { public async sync(): Promise { const auth = this.getAuthOrThrow() - const result = await syncDown({ - auth, - storage: this.storage, - logFormat: this.config.logFormat, - }) - - this.synced = true - return result + try{ + const result = await syncDown({ + auth, + storage: this.storage, + logFormat: this.config.logFormat, + }) + if (result.error) { + this.log.error('Sync error:', result.error) + throw new KeeperSdkError(`Sync failed: ${result.error}`, ResultCodes.SYNC_FAILED) + } + this.synced = true + return result + }catch(e) { + this.log.error('Sync failed:', extractErrorMessage(e)) + throw e + } } public async batch(fn: () => Promise): Promise { diff --git a/examples/sdk_example/README.md b/examples/sdk_example/README.md index dd54fb67..996c12f0 100644 --- a/examples/sdk_example/README.md +++ b/examples/sdk_example/README.md @@ -35,7 +35,7 @@ For restore-session flows, provide a path to session JSON (extension `SessionPar | `npm run auth:login` | Master password login with retry logic, masked input, and vault sync. Automatically attempts persistent login (via clone code from `~/.keeper/config.json`) before falling back to the password prompt. | | `npm run auth:session-token` | Login using an existing session token for pre-authenticated workflows. Prompts for username, host, and session token. Requires device token + private key in `~/.keeper/config.json` or use `auth:register-device` first in the same run. | | `npm run auth:register-device` | Store device token + device private key on a `KeeperVault` (in-memory), optionally then `loginWithSessionToken` + sync. | -| `npm run auth:restore-session` | Restore extension-style session JSON (prompts for file path; required), then syncDown. No device keys or `loginV3`. | +| `npm run auth:restore-session` | Restore via SDK `restore-session` CLI dispatch (same path as shellcomponent). Prompts for session JSON path. | ### Records @@ -68,10 +68,19 @@ npm run records:get Most examples will log in automatically using persistent login (if configured) or prompt for credentials. After authentication, follow the interactive prompts. -**Restore-session flag** on `records:list` (requires `--from-json`; no default path): +**Restore-session flag** on `records:list` (requires `--from-json`; no default path). Uses the same SDK CLI dispatch as shellcomponent (`dispatchCliLine` → `restore-session`): ```bash npm run records:list -- --restore-session --from-json /path/to/session.json npm run records:list -- --restore-session --from-json /path/to/session.json --host keepersecurity.eu npm run records:list -- --restore-session --from-json /path/to/session.json --no-sync ``` + +**Shell-parity debug** — restore and list only through CLI commands (closest match to typing in keeper-shell): + +```bash +npm run records:list:shell-cli -- --from-json /path/to/session.json +npm run records:list:shell-cli -- --from-json /path/to/session.json --host keepersecurity.eu +``` + +If Node succeeds but the browser shell fails, the difference is likely host I/O (`readTextFile` / Vite `/@fs`), CORS, or region (`keeper-host` / `KEEPER_HOST`), not `KeeperVault.restoreSession` itself. diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index 9d614a22..c5893e50 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -8,6 +8,7 @@ "auth:register-device": "ts-node src/auth/register_device.ts", "auth:restore-session": "ts-node src/auth/restore_session.ts", "records:list": "ts-node src/records/list_records.ts", + "records:list:shell-cli": "ts-node src/records/list_records_shell_cli.ts", "records:get": "ts-node src/records/get_record.ts", "records:add": "ts-node src/records/add_record.ts", "records:update": "ts-node src/records/update_record.ts", diff --git a/examples/sdk_example/src/records/list_records_shell_cli.ts b/examples/sdk_example/src/records/list_records_shell_cli.ts new file mode 100644 index 00000000..53536047 --- /dev/null +++ b/examples/sdk_example/src/records/list_records_shell_cli.ts @@ -0,0 +1,36 @@ +import { cleanup, logger } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { assertRestoreCliArgs, parseRestoreCliArgs, requireSessionJsonPath } from '../utils/restoreAuth' +import { listRecordsViaShellCli, loginViaShellCliRestoreSession } from '../utils/shellCliRestore' + +/** + * Debug script: restore + list entirely through SDK CLI dispatch (mirrors shellcomponent). + * + * npm run records:list:shell-cli -- --from-json /path/to/session.json + * npm run records:list:shell-cli -- --from-json /path/to/session.json --host keepersecurity.eu + */ +async function main() { + const cli = parseRestoreCliArgs() + if (!cli.restoreSession && !cli.jsonPath) { + throw new Error( + 'records:list:shell-cli requires --from-json /path/to/session.json (shell-style restore-session flow)' + ) + } + assertRestoreCliArgs({ ...cli, restoreSession: true }) + + const jsonPath = requireSessionJsonPath(cli.jsonPath, 'records:list:shell-cli') + const vault = await loginViaShellCliRestoreSession({ + jsonPath, + host: cli.host, + sync: !cli.noSync, + }) + + try { + const out = await listRecordsViaShellCli() + logger.info(out.trimEnd()) + } finally { + cleanup(vault) + } +} + +runExample(main) diff --git a/examples/sdk_example/src/utils/inMemoryConfigLoader.ts b/examples/sdk_example/src/utils/inMemoryConfigLoader.ts new file mode 100644 index 00000000..8a0c8422 --- /dev/null +++ b/examples/sdk_example/src/utils/inMemoryConfigLoader.ts @@ -0,0 +1,16 @@ +import type { ConfigLoader, KeeperJsonConfig } from '@keeper-security/keeper-sdk-javascript' + +/** In-memory session/device storage (same role as shellcomponent's InMemoryConfigLoader). */ +export class InMemoryConfigLoader implements ConfigLoader { + public readonly configDir = '' + + private data: KeeperJsonConfig = {} + + async load(): Promise { + return JSON.parse(JSON.stringify(this.data)) as KeeperJsonConfig + } + + async save(config: KeeperJsonConfig): Promise { + this.data = JSON.parse(JSON.stringify(config)) as KeeperJsonConfig + } +} diff --git a/examples/sdk_example/src/utils/restoreAuth.ts b/examples/sdk_example/src/utils/restoreAuth.ts index 454beb2e..bc09bd6c 100644 --- a/examples/sdk_example/src/utils/restoreAuth.ts +++ b/examples/sdk_example/src/utils/restoreAuth.ts @@ -1,14 +1,6 @@ -import * as fs from 'fs/promises' import * as path from 'path' -import { - KeeperVault, - KeeperSdkError, - logger, - SdkDefaults, - suppressLogs, - resolveSessionRestorePayload, - ResultCodes, -} from '@keeper-security/keeper-sdk-javascript' +import { KeeperSdkError, ResultCodes, type KeeperVault } from '@keeper-security/keeper-sdk-javascript' +import { loginViaShellCliRestoreSession } from './shellCliRestore' export type RestoreSessionLoginOptions = { /** Path to extension-style session JSON (required). */ @@ -31,48 +23,15 @@ export function requireSessionJsonPath(jsonPath: string | undefined, context: st } /** - * Authenticate via SessionParams JSON + continueSession (no password / device login). + * Restore session via SDK CLI dispatch (same path as shellcomponent `restore-session`). */ export async function loginViaRestoreSession(options: RestoreSessionLoginOptions): Promise { const jsonPath = requireSessionJsonPath(options.jsonPath, 'restore-session') - - try { - await fs.access(jsonPath) - } catch { - throw new KeeperSdkError(`Session JSON file not found: ${jsonPath}`, ResultCodes.INVALID_CREDENTIALS) - } - - const host = options.host?.trim() || 'keepersecurity.com' - const runSync = options.sync !== false - - logger.info(`Restoring session from ${jsonPath} (${host})...`) - - const vault = new KeeperVault({ host, clientVersion: SdkDefaults.CLIENT_VERSION }) - const input = await resolveSessionRestorePayload(jsonPath, (p) => fs.readFile(p, 'utf8')) - - const restore = suppressLogs() - try { - await vault.restoreSession(input) - } finally { - restore() - } - - logger.info(`Authenticated as ${input.username} (restore-session).`) - - if (runSync) { - logger.info('Syncing vault...') - const restoreSync = suppressLogs() - try { - const result = await vault.sync() - logger.info( - `Vault synced (${result.pageCount} page${result.pageCount === 1 ? '' : 's'}, ${vault.getSummary().recordCount} records).\n` - ) - } finally { - restoreSync() - } - } - - return vault + return loginViaShellCliRestoreSession({ + jsonPath, + host: options.host, + sync: options.sync, + }) } export type RestoreCliArgs = { diff --git a/examples/sdk_example/src/utils/shellCliHost.ts b/examples/sdk_example/src/utils/shellCliHost.ts new file mode 100644 index 00000000..515a773d --- /dev/null +++ b/examples/sdk_example/src/utils/shellCliHost.ts @@ -0,0 +1,100 @@ +import * as fs from 'fs/promises' +import type { KeeperCliHost, KeeperCliVault } from '@keeper-security/keeper-sdk-javascript' +import { + KeeperVault, + LogLevel, + SessionManager, + SdkDefaults, +} from '@keeper-security/keeper-sdk-javascript' +import { InMemoryConfigLoader } from './inMemoryConfigLoader' + +type VaultInstance = KeeperVault + +let keeperHost: string | undefined +let vault: VaultInstance | null = null + +/** Optional region/host override (shell: keeper-host / KEEPER_HOST). */ +export function setExampleKeeperHost(host: string | undefined): void { + const trimmed = host?.trim() + keeperHost = trimmed || undefined + vault = null +} + +export function getExampleKeeperHost(): string | undefined { + if (keeperHost) return keeperHost + const fromEnv = (process.env.KEEPER_HOST || '').trim() + return fromEnv || undefined +} + +export function resetExampleShellVault(): void { + vault = null +} + +function getVault(): VaultInstance { + if (!vault) { + const host = getExampleKeeperHost() + vault = new KeeperVault({ + ...(host ? { host } : {}), + useConsoleAuth: false, + logLevel: LogLevel.WARN, + clientVersion: SdkDefaults.CLIENT_VERSION, + sessionStorage: new SessionManager(new InMemoryConfigLoader()), + }) + } + return vault +} + +/** Underlying vault instance (for cleanup() after CLI dispatch). */ +export function getExampleKeeperVault(): VaultInstance { + return getVault() +} + +function errMsg(e: unknown): string { + return e instanceof Error ? e.message : String(e) +} + +function formatKeeperClientError(context: string, e: unknown): string { + return `${context}: ${errMsg(e)}\n` +} + +function asCliVault(v: VaultInstance): KeeperCliVault { + return { + get isLoggedIn() { + return v.isLoggedIn + }, + login: (u, p) => v.login(u, p), + loginWithSessionToken: (u, t) => v.loginWithSessionToken(u, t), + logout: () => v.logout(), + sync: () => v.sync(), + getRecords: () => v.getRecords(), + getSharedFolders: () => v.getSharedFolders(), + registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), + restoreSession: (input) => v.restoreSession(input), + } +} + +async function readTextFile(filePath: string): Promise { + const p = filePath.trim().replace(/^@/, '') + if (/^https?:\/\//i.test(p)) { + const res = await fetch(p) + if (!res.ok) { + throw new Error(`HTTP ${res.status} loading ${p}`) + } + return res.text() + } + const resolved = p.replace(/^~/, process.env.HOME || '') + return fs.readFile(resolved, 'utf8') +} + +function envString(name: string): string | undefined { + const v = process.env[name] + return typeof v === 'string' && v.length > 0 ? v : undefined +} + +/** Node adapter matching shellcomponent/src/cli/keeperCliHost.ts (fs instead of Vite fetch). */ +export const exampleShellCliHost: KeeperCliHost = { + getVault: () => asCliVault(getVault()), + envString, + formatError: formatKeeperClientError, + readTextFile, +} diff --git a/examples/sdk_example/src/utils/shellCliRestore.ts b/examples/sdk_example/src/utils/shellCliRestore.ts new file mode 100644 index 00000000..bb0017ee --- /dev/null +++ b/examples/sdk_example/src/utils/shellCliRestore.ts @@ -0,0 +1,83 @@ +import * as path from 'path' +import { + dispatchCliLine, + KeeperSdkError, + logger, + ResultCodes, + type CliResult, + type KeeperVault, +} from '@keeper-security/keeper-sdk-javascript' +import { + exampleShellCliHost, + getExampleKeeperVault, + resetExampleShellVault, + setExampleKeeperHost, +} from './shellCliHost' + +export type ShellCliRestoreOptions = { + /** Resolved or raw path to session JSON (caller should validate). */ + jsonPath: string + host?: string + /** Run `restore-session --sync` (default true). */ + sync?: boolean +} + +function resolveJsonPathForCli(jsonPath: string): string { + const expanded = jsonPath.trim().replace(/^~/, process.env.HOME || '') + return path.isAbsolute(expanded) ? expanded : path.resolve(expanded) +} + +export function buildRestoreSessionCliLine(jsonPath: string, sync: boolean): string { + const abs = resolveJsonPathForCli(jsonPath) + let line = `restore-session --from-json ${abs}` + if (sync) line += ' --sync' + return line +} + +function throwOnCliFailure(label: string, result: CliResult): void { + if (result.code === 0) return + const detail = (result.err || result.out).trim() + throw new KeeperSdkError( + `${label} failed (exit ${result.code})${detail ? `: ${detail}` : ''}`, + ResultCodes.INVALID_CREDENTIALS + ) +} + +/** + * Authenticate via SDK `restore-session` CLI (same dispatch path as shellcomponent). + */ +export async function loginViaShellCliRestoreSession( + options: ShellCliRestoreOptions +): Promise { + const jsonPath = options.jsonPath + const runSync = options.sync !== false + + resetExampleShellVault() + setExampleKeeperHost(options.host) + + const line = buildRestoreSessionCliLine(jsonPath, runSync) + logger.info(`[shell-cli] ${line}`) + + const result = await dispatchCliLine(line, exampleShellCliHost) + throwOnCliFailure('restore-session', result) + if (result.out.trim()) { + logger.info(result.out.trimEnd()) + } + + const vault = getExampleKeeperVault() + if (!vault.isLoggedIn) { + throw new KeeperSdkError( + 'restore-session completed but vault is not logged in', + ResultCodes.SESSION_TOKEN_EXPIRED + ) + } + return vault +} + +/** Run `records list` through CLI dispatch (shell-style listing). */ +export async function listRecordsViaShellCli(): Promise { + logger.info('[shell-cli] records list') + const result = await dispatchCliLine('records list', exampleShellCliHost) + throwOnCliFailure('records list', result) + return result.out +} diff --git a/shellcomponent/README.md b/shellcomponent/README.md index c174bb3f..c8f5576d 100644 --- a/shellcomponent/README.md +++ b/shellcomponent/README.md @@ -100,6 +100,28 @@ npm run dev Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell. +### Same-origin dev (in-browser SDK, no separate Node API) + +The dev page runs the **Keeper SDK in the browser** (no **`remote`** / **`api-base`**). Cross-origin calls to `keepersecurity.com` would fail CORS from `localhost`, so dev wires a **same-origin proxy**: + +1. **`installKeeperSameOriginProxy`** (in `dev-bootstrap.ts`) rewrites `fetch` / `WebSocket` URLs to + `http://localhost:5175/__keeper//…` and `ws://localhost:5175/__keeper-wss//…` +2. **`keeperSameOriginProxyPlugin`** (Vite) forwards those paths to the real Keeper HTTPS/WSS hosts. + +Only **`npm run dev`** is required. Example: + +```text +restore-session --from-json /dev/keeper-session.json --sync +records list +``` + +Use an absolute path to repo `conf.json` if you prefer: +`restore-session --from-json /Users/you/.../keeper-sdk-javascript/conf.json --sync` + +Set region on the element when needed: ``. + +**Production:** your deployed origin must be allowed by Keeper’s CORS policy, or you host a trusted API and use **`remote`** + **`api-base`**. The Vite proxy is for **local dev only**. + ```bash npm run build ``` diff --git a/shellcomponent/index.html b/shellcomponent/index.html index 332ee899..59f8c953 100644 --- a/shellcomponent/index.html +++ b/shellcomponent/index.html @@ -26,6 +26,7 @@

Web console (dev)

+ diff --git a/shellcomponent/package-lock.json b/shellcomponent/package-lock.json index 240fd6d6..223a3594 100644 --- a/shellcomponent/package-lock.json +++ b/shellcomponent/package-lock.json @@ -16,8 +16,10 @@ "protobufjs": "^7.2.4" }, "devDependencies": { + "@types/http-proxy": "^1.17.15", "@types/node": "^22.10.2", "buffer": "^6.0.3", + "http-proxy": "^1.18.1", "path-browserify": "^1.0.1", "typescript": "~5.7.2", "vite": "^6.0.3" @@ -25,7 +27,7 @@ }, "../keeperapi": { "name": "@keeper-security/keeperapi", - "version": "17.1.2", + "version": "17.1.3", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", @@ -936,6 +938,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "22.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", @@ -1048,6 +1060,13 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1066,6 +1085,27 @@ } } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1081,6 +1121,21 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1208,6 +1263,13 @@ "node": ">=12.0.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", diff --git a/shellcomponent/package.json b/shellcomponent/package.json index ccca00d7..b83e118b 100644 --- a/shellcomponent/package.json +++ b/shellcomponent/package.json @@ -37,6 +37,8 @@ "devDependencies": { "@types/node": "^22.10.2", "buffer": "^6.0.3", + "http-proxy": "^1.18.1", + "@types/http-proxy": "^1.17.15", "path-browserify": "^1.0.1", "typescript": "~5.7.2", "vite": "^6.0.3" diff --git a/shellcomponent/src/dev-bootstrap.ts b/shellcomponent/src/dev-bootstrap.ts index f0952882..93d717f5 100644 --- a/shellcomponent/src/dev-bootstrap.ts +++ b/shellcomponent/src/dev-bootstrap.ts @@ -3,6 +3,9 @@ * (static `import "./KeeperShell.js"` would fail the whole module with no UI feedback). */ import "./bufferPolyfill.js"; +import { installKeeperSameOriginProxy } from "./dev/installKeeperSameOriginProxy.js"; + +installKeeperSameOriginProxy(); function showShellLoadError(err: unknown): void { const msg = err instanceof Error ? `${err.message}\n\n${err.stack ?? ""}` : String(err); diff --git a/shellcomponent/src/dev/installKeeperSameOriginProxy.ts b/shellcomponent/src/dev/installKeeperSameOriginProxy.ts new file mode 100644 index 00000000..bf12ea5a --- /dev/null +++ b/shellcomponent/src/dev/installKeeperSameOriginProxy.ts @@ -0,0 +1,69 @@ +import { + isAllowedKeeperProxyHost, + KEEPER_REST_PROXY_PREFIX, + KEEPER_WSS_PROXY_PREFIX, +} from "./keeperProxyHosts.js"; + +function shouldProxyUrl(url: URL): boolean { + return isAllowedKeeperProxyHost(url.hostname); +} + +function rewriteKeeperUrl(url: string): string { + try { + const u = new URL(url, typeof window !== "undefined" ? window.location.href : undefined); + if (!shouldProxyUrl(u)) return url; + + const ws = u.protocol === "wss:" || u.protocol === "ws:"; + const prefix = ws ? KEEPER_WSS_PROXY_PREFIX : KEEPER_REST_PROXY_PREFIX; + const proto = + window.location.protocol === "https:" + ? ws + ? "wss:" + : "https:" + : ws + ? "ws:" + : "http:"; + + return `${proto}//${window.location.host}${prefix}${u.host}${u.pathname}${u.search}`; + } catch { + return url; + } +} + +/** + * Dev-only: route Keeper REST/WebSocket through the Vite origin (`/__keeper/…`, `/__keeper-wss/…`). + * SDK and CLI stay in the browser; Vite proxies to Keeper (same-origin from the page’s view). + */ +export function installKeeperSameOriginProxy(): void { + if (typeof window === "undefined") return; + + const nativeFetch = window.fetch.bind(window); + window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { + if (typeof input === "string") { + return nativeFetch(rewriteKeeperUrl(input), init); + } + if (input instanceof Request) { + const next = rewriteKeeperUrl(input.url); + return next === input.url + ? nativeFetch(input, init) + : nativeFetch(new Request(next, input), init); + } + return nativeFetch(input, init); + }) as typeof fetch; + + const NativeWebSocket = window.WebSocket; + window.WebSocket = new Proxy(NativeWebSocket, { + construct(_target, args: [string | URL, (string | string[])?]) { + const [url, protocols] = args; + return new NativeWebSocket(rewriteKeeperUrl(String(url)), protocols); + }, + }) as typeof WebSocket; + + if (import.meta.env?.DEV === true) { + console.info( + "[keeper-shell] Same-origin dev proxy enabled:", + KEEPER_REST_PROXY_PREFIX, + KEEPER_WSS_PROXY_PREFIX + ); + } +} diff --git a/shellcomponent/src/dev/keeperProxyHosts.ts b/shellcomponent/src/dev/keeperProxyHosts.ts new file mode 100644 index 00000000..fbb1ced3 --- /dev/null +++ b/shellcomponent/src/dev/keeperProxyHosts.ts @@ -0,0 +1,20 @@ +/** Dev-only: allow proxying to known Keeper infrastructure hosts. */ +export function isAllowedKeeperProxyHost(host: string): boolean { + if (!host || host.includes("..") || host.includes("/")) return false; + if (/^push\.services\./.test(host)) { + return isAllowedKeeperProxyHost(host.slice("push.services.".length)); + } + if (/^connect\./.test(host)) { + const rest = host.slice("connect.".length); + return ( + rest === "keepersecurity.us" || + /^[a-z0-9.-]+\.keepersecurity\.(com|eu|us)$/.test(rest) || + rest === "keepersecurity.com" || + rest === "keepersecurity.eu" + ); + } + return /^([a-z0-9.-]+\.)?keepersecurity\.(com|eu|us)$/.test(host); +} + +export const KEEPER_REST_PROXY_PREFIX = "/__keeper/"; +export const KEEPER_WSS_PROXY_PREFIX = "/__keeper-wss/"; diff --git a/shellcomponent/vite.config.ts b/shellcomponent/vite.config.ts index 99d8536c..b0a7ee9a 100644 --- a/shellcomponent/vite.config.ts +++ b/shellcomponent/vite.config.ts @@ -2,6 +2,7 @@ import { readFileSync, existsSync } from "node:fs"; import { defineConfig, type Plugin } from "vite"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { keeperSameOriginProxyPlugin } from "./vite/keeperSameOriginProxyPlugin.js"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); const repoConfJson = resolve(__dirname, "../conf.json"); @@ -30,7 +31,7 @@ function keeperDevSessionConfPlugin(): Plugin { export default defineConfig({ root: ".", - plugins: [keeperDevSessionConfPlugin()], + plugins: [keeperDevSessionConfPlugin(), keeperSameOriginProxyPlugin()], resolve: { alias: { "@keeper-security/keeper-sdk-javascript": resolve(__dirname, "../KeeperSdk/src/browser.ts"), diff --git a/shellcomponent/vite/keeperSameOriginProxyPlugin.ts b/shellcomponent/vite/keeperSameOriginProxyPlugin.ts new file mode 100644 index 00000000..56957a0a --- /dev/null +++ b/shellcomponent/vite/keeperSameOriginProxyPlugin.ts @@ -0,0 +1,57 @@ +import type { Plugin } from "vite"; +import httpProxy from "http-proxy"; +import { isAllowedKeeperProxyHost } from "../src/dev/keeperProxyHosts.js"; + +/** + * Vite dev server: forward same-origin `/__keeper//…` and `/__keeper-wss//…` + * to real Keeper HTTPS/WSS endpoints (avoids browser CORS while SDK runs in-page). + */ +export function keeperSameOriginProxyPlugin(): Plugin { + const proxy = httpProxy.createProxyServer({ + changeOrigin: true, + secure: true, + ws: true, + }); + + return { + name: "keeper-same-origin-proxy", + configureServer(server) { + server.middlewares.use((req, res, next) => { + const raw = req.url ?? ""; + const match = raw.match(/^\/__keeper\/([^/]+)(\/.*)?$/); + if (!match) { + next(); + return; + } + const targetHost = match[1]; + if (!isAllowedKeeperProxyHost(targetHost)) { + res.statusCode = 403; + res.end("Forbidden keeper proxy host"); + return; + } + req.url = match[2] ?? "/"; + proxy.web( + req, + res, + { target: `https://${targetHost}` }, + (err) => { + if (err) next(err); + } + ); + }); + + server.httpServer?.on("upgrade", (req, socket, head) => { + const raw = req.url ?? ""; + const match = raw.match(/^\/__keeper-wss\/([^/]+)(\/.*)?$/); + if (!match) return; + const targetHost = match[1]; + if (!isAllowedKeeperProxyHost(targetHost)) { + socket.destroy(); + return; + } + req.url = match[2] ?? "/"; + proxy.ws(req, socket, head, { target: `wss://${targetHost}` }); + }); + }, + }; +} From 2da052d9b1ebec463780dbcf7ca21d2c0e050cbe Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 25 May 2026 17:35:07 +0530 Subject: [PATCH 07/23] added a working copy of code changes --- KeeperSdk/src/api.ts | 4 +- KeeperSdk/src/cli/commands/folders.ts | 291 +++++++++++++++--- KeeperSdk/src/cli/commands/login.ts | 28 +- KeeperSdk/src/cli/commands/logout.ts | 3 +- KeeperSdk/src/cli/commands/records.ts | 4 - KeeperSdk/src/cli/commands/registerDevice.ts | 6 +- KeeperSdk/src/cli/commands/restoreSession.ts | 11 +- KeeperSdk/src/cli/commands/sync.ts | 6 +- KeeperSdk/src/cli/help.ts | 15 +- KeeperSdk/src/cli/index.ts | 5 +- KeeperSdk/src/cli/parser.ts | 199 ++++++++++++ KeeperSdk/src/cli/types.ts | 17 +- KeeperSdk/src/vault/KeeperVault.ts | 19 ++ .../sdk_example/src/utils/shellCliHost.ts | 7 + shellcomponent/src/cli/keeperCliHost.ts | 7 + 15 files changed, 536 insertions(+), 86 deletions(-) create mode 100644 KeeperSdk/src/cli/parser.ts diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts index 7875bf3a..cc450b95 100644 --- a/KeeperSdk/src/api.ts +++ b/KeeperSdk/src/api.ts @@ -201,8 +201,10 @@ export { loginWithSessionToken, runLoginCommand, runLogoutCommand, - KEEPER_VAULT_SURFACE, + KeeperCliParser, + createKeeperCliParser, } from './cli' +export type { KeeperCliParserOptions } from './cli' export type { CliResult, ParsedCli, diff --git a/KeeperSdk/src/cli/commands/folders.ts b/KeeperSdk/src/cli/commands/folders.ts index fe47dde4..90b186f9 100644 --- a/KeeperSdk/src/cli/commands/folders.ts +++ b/KeeperSdk/src/cli/commands/folders.ts @@ -1,57 +1,274 @@ import type { DSharedFolder } from '@keeper-security/keeperapi' -import type { CliCommandDefinition, KeeperCliHost } from '../types' -import { wantsCliHelp } from '../parse' +import type { CliCommandDefinition, CliResult, KeeperCliHost, KeeperCliVault, ParsedCli } from '../types' +import { hasOpt, wantsCliHelp } from '../parse' import { formatDetailedHelpForCommand } from '../help' import { ensureLoggedIn } from './login' +const SUBCOMMANDS = ['list', 'tree', 'ls', 'pwd', 'cd', 'mkdir', 'get'] as const +type Sub = (typeof SUBCOMMANDS)[number] + +function ensureCapability( + v: KeeperCliVault, + name: K, + sub: string +): CliResult | null { + if (typeof v[name] !== 'function') { + return { + code: 1, + out: '', + err: `folders ${sub}: this host does not expose KeeperCliVault.${String(name)}.\n`, + } + } + return null +} + +async function ensureSession(host: KeeperCliHost): Promise { + const v = host.getVault() + if (v.isLoggedIn) return null + const r = await ensureLoggedIn(host) + return r.code === 0 ? null : r +} + +/** Fixed-width column formatter. Last column is left unpadded so trailing whitespace is avoided. */ +function formatTable(headers: string[], rows: string[][]): string { + if (rows.length === 0) return '' + const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))) + const fmt = (cells: string[]): string => + cells.map((s, i) => (i === cells.length - 1 ? s : (s ?? '').padEnd(widths[i]))).join(' ') + return [fmt(headers), ...rows.map(fmt)].join('\n') + '\n' +} + +async function runListShared(host: KeeperCliHost): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + await v.sync() + const folders = v.getSharedFolders() + if (folders.length === 0) return { code: 0, out: '(no shared folders)\n', err: '' } + const rows = folders.map((f: DSharedFolder) => [f.uid ?? '(unknown uid)', f.name ?? '(unnamed)']) + return { code: 0, out: formatTable(['shared_folder_uid', 'name'], rows), err: '' } +} + +async function runTree(host: KeeperCliHost): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'tree', 'tree') + if (cap) return cap + await v.sync() + const out = await v.tree!() + return { code: 0, out: out.endsWith('\n') ? out : out + '\n', err: '' } +} + +async function runLs(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listFolder', 'ls') + if (cap) return cap + await v.sync() + const target = parsed.positional[1] + const detail = hasOpt(parsed.opts, 'detail') + + if (!target) { + const result = await v.listFolder!({ detail }) + return { code: 0, out: formatLs(result, detail), err: '' } + } + + if (!v.changeDirectory || !v.getCurrentFolderUid) { + return { code: 1, out: '', err: 'folders ls : host lacks navigation capabilities.\n' } + } + + // Snapshot cwd, resolve via cd, list, then restore. ls is read-only; + // path resolution should never leave the session somewhere new. + const originalUid = v.getCurrentFolderUid() + let resolvedUid: string | null + try { + const cd = await v.changeDirectory(target) + resolvedUid = cd.folderUid + } catch (e) { + return { code: 1, out: '', err: host.formatError(`folders ls ${target}`, e) } + } + try { + const result = await v.listFolder!({ folderUid: resolvedUid ?? null, detail }) + return { code: 0, out: formatLs(result, detail), err: '' } + } finally { + if (resolvedUid !== originalUid) { + try { + await v.changeDirectory(originalUid ?? '/') + } catch { + // best-effort restore; user can `folders cd /` if needed + } + } + } +} + +function formatLs( + result: { + detail: boolean + folders: Array<{ uid: string; name: string }> + records: Array<{ uid: string; name: string; type?: string }> + }, + detail: boolean +): string { + if (result.folders.length + result.records.length === 0) return '(empty)\n' + + const headers = detail ? ['flags', 'uid', 'name', 'type'] : ['kind', 'uid', 'name'] + const rows: string[][] = [] + for (const f of result.folders) { + const flags = ((f as { flags?: string }).flags ?? '').trim() + rows.push(detail ? [flags || 'f---', f.uid, f.name, ''] : ['dir', f.uid, f.name]) + } + for (const r of result.records) { + const flags = ((r as { flags?: string }).flags ?? '').trim() + const type = r.type ?? '' + rows.push(detail ? [flags || 'r---', r.uid, r.name, type] : ['rec', r.uid, r.name]) + } + return formatTable(headers, rows) +} + +async function runPwd(host: KeeperCliHost): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'getWorkingFolderDisplayName', 'pwd') + if (cap) return cap + return { code: 0, out: `${v.getWorkingFolderDisplayName!()}\n`, err: '' } +} + +async function runCd(host: KeeperCliHost, parsed: ParsedCli): Promise { + const target = parsed.positional[1] + if (!target) return { code: 1, out: '', err: 'folders cd: missing path. Usage: folders cd \n' } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'changeDirectory', 'cd') + if (cap) return cap + try { + const res = await v.changeDirectory!(target) + return { code: 0, out: `${res.name}\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`folders cd ${target}`, e) } + } +} + +async function runMkdir(host: KeeperCliHost, parsed: ParsedCli): Promise { + const target = parsed.positional[1] + if (!target) { + return { + code: 1, + out: '', + err: 'folders mkdir: missing path. Usage: folders mkdir [--shared]\n', + } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'mkdir', 'mkdir') + if (cap) return cap + const cwd = v.getWorkingFolderDisplayName?.() ?? '(root)' + try { + const res = await v.mkdir!(target, { sharedFolder: hasOpt(parsed.opts, 'shared') }) + if (!res.success) { + return { code: 1, out: '', err: `folders mkdir [in ${cwd}]: ${res.message ?? 'failed'}\n` } + } + return { code: 0, out: `${res.folderUid}\t${target} (in ${cwd})\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`folders mkdir ${target} [in ${cwd}]`, e) } + } +} + +async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise { + const target = parsed.positional[1] + if (!target) return { code: 1, out: '', err: 'folders get: missing UID or name. Usage: folders get \n' } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'getFolder', 'get') + if (cap) return cap + try { + const res = await v.getFolder!(target, { format: 'json' }) + const json = (res as { json?: Record }).json ?? res + return { code: 0, out: JSON.stringify(json, null, 2) + '\n', err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`folders get ${target}`, e) } + } +} + export const foldersCommand: CliCommandDefinition = { name: 'folders', order: 31, - description: 'List shared folders in the vault (uses env or `login --username …`).', - usage: 'folders [list] [--help|-h]', - subcommands: ['list'], + description: 'List/navigate vault folders (list, tree, ls, pwd, cd, mkdir, get).', + usage: 'folders [list|tree|ls|pwd|cd|mkdir|get] [args] [--help|-h]', + subcommands: [...SUBCOMMANDS], + flagOptions: ['--shared', '--detail'], help: { - title: 'folders — list shared folders', - synopsis: ' folders [list]', - description: ' Runs sync, then prints shared_folder_uid and name for each shared folder.', - arguments: ' list Optional; default is list.', - options: ' --help, -h Show this help.', - keeperSdk: ` Uses KeeperVault.sync(), getSharedFolders(). - Related: listSharedFolders, shareFolder, FolderManager / SharedFolderManager.`, - appendVaultSurface: true, + title: 'folders — navigate and inspect vault folders', + synopsis: ` folders [list] Top-level shared folders (uid + name) + folders tree Render the full folder tree + folders ls [PATH] [--detail] Contents of a folder (default: current) + folders pwd Print current working folder display name + folders cd PATH Change current folder + folders mkdir PATH [--shared] Create a user (or shared) folder + folders get UID|NAME Print folder details as JSON`, + description: ` Folder navigation maintained per-shell. The current folder affects + subsequent ls, mkdir, and other folder-relative operations. + + PATH is a slash-separated sequence of folder names or UIDs (e.g. + "Marketing/Q3" or "AAAA-bbbb-uid"). "/" is the vault root. + + list (default) calls sync() then enumerates shared folders only — + useful for a quick top-level view. Use tree or ls for full contents.`, + arguments: ` list (default) Print shared_folder_uid + name for top-level shared folders. + tree Render the full folder tree (folders + records) as ASCII. + ls PATH List immediate children (folders + records). Default: current folder. + pwd Print the current working folder. + cd PATH Change current folder. "/" returns to root. + mkdir PATH [--shared] Create a folder. --shared makes it a shared folder. + get UID|NAME Print folder metadata as JSON.`, + options: ` --detail ls only: include flags, record types, ownership. + --shared mkdir only: create a shared folder instead of a user folder. + --help, -h Show this help.`, + examples: ` folders # shared folders quick list + folders tree # full tree + folders ls Marketing # contents of Marketing + folders cd Marketing/Q3 # navigate + folders mkdir Drafts # user folder under cwd + folders mkdir Public --shared # new shared folder under cwd + folders get aaaa-bbbb-uid # folder details as JSON`, + seeAlso: ' records list, sync, login, restore-session', }, async run(host, parsed) { if (wantsCliHelp(parsed)) { return { code: 0, out: formatDetailedHelpForCommand(foldersCommand), err: '' } } - if (parsed.opts.size > 0) { - return { code: 1, out: '', err: 'folders: unknown option (try: folders --help)\n' } - } - const sub = parsed.positional[0]?.toLowerCase() - if (parsed.positional.length > 1) { - return { code: 1, out: '', err: 'Usage: folders [list]\n' } - } - if (sub && sub !== 'list') { - return { code: 1, out: '', err: 'Usage: folders [list]\n' } + const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub + if (!SUBCOMMANDS.includes(sub)) { + return { + code: 1, + out: '', + err: `folders: unknown subcommand "${parsed.positional[0]}". Try: folders --help\n`, + } } try { - const v = host.getVault() - if (!v.isLoggedIn) { - const r = await ensureLoggedIn(host) - if (r.code !== 0) return r + switch (sub) { + case 'list': + return await runListShared(host) + case 'tree': + return await runTree(host) + case 'ls': + return await runLs(host, parsed) + case 'pwd': + return await runPwd(host) + case 'cd': + return await runCd(host, parsed) + case 'mkdir': + return await runMkdir(host, parsed) + case 'get': + return await runGet(host, parsed) } - await v.sync() - const folders = v.getSharedFolders() - const rows = folders.map((f: DSharedFolder) => { - const name = f.name ?? '(unnamed)' - const uid = f.uid ?? '(unknown uid)' - return `${uid}\t${name}` - }) - const header = 'shared_folder_uid\tname\n' - const body = rows.length ? rows.join('\n') + '\n' : '(no shared folders)\n' - return { code: 0, out: header + body, err: '' } } catch (e) { - return { code: 1, out: '', err: host.formatError('folders', e) } + return { code: 1, out: '', err: host.formatError(`folders ${sub}`, e) } } }, } diff --git a/KeeperSdk/src/cli/commands/login.ts b/KeeperSdk/src/cli/commands/login.ts index ab4d4540..4d2313f1 100644 --- a/KeeperSdk/src/cli/commands/login.ts +++ b/KeeperSdk/src/cli/commands/login.ts @@ -116,12 +116,22 @@ export async function loginWithSessionToken( } } -/** Attempt env/password login when vault commands need a session. */ +/** + * Returns success if a session is active. Otherwise emits a plain + * "not logged in" error — no login flags or prompts. Commands that want + * env-based auto-login should call runLoginCommand explicitly. + * + * Auto-login is preserved only when KEEPER_USERNAME is present in env; + * runLoginCommand then handles password/session-token resolution. + */ export async function ensureLoggedIn(host: KeeperCliHost): Promise { if (host.getVault().isLoggedIn) { return { code: 0, out: '', err: '' } } - return runLoginCommand(host, { positional: [], opts: new Map() }) + if (host.envString('KEEPER_USERNAME')) { + return runLoginCommand(host, { positional: [], opts: new Map() }) + } + return { code: 1, out: '', err: 'not logged in\n' } } export const loginCommand: CliCommandDefinition = { @@ -145,7 +155,7 @@ export const loginCommand: CliCommandDefinition = { synopsis: ` login [--username|--user EMAIL_OR_NAME] login [--username|--user U] [--session-token|--token|--st TOKEN] login [--username|--user U] [--session-token TOKEN] [--session-token-plain]`, - description: ` Establishes a Keeper session using KeeperVault. + description: ` Establishes a Keeper session. Username comes from --username / --user or KEEPER_USERNAME. @@ -154,12 +164,11 @@ export const loginCommand: CliCommandDefinition = { Web shell: run login with only a username; the UI prompts for a masked password and sends it through the login transport, not in "line". - Session token login uses KeeperVault.loginWithSessionToken. The token may be - passed on the command line or via KEEPER_SESSION_TOKEN (sensitive — same - caveats as any secret on argv). + Session token login: pass the token on the command line or via + KEEPER_SESSION_TOKEN (sensitive — same caveats as any secret on argv). - --session-token-plain encodes the token from UTF-8 to base64url before the - SDK call (same idea as the session_token_login example when the token is raw text). + --session-token-plain treats the value as plain UTF-8 and encodes base64url + before login (same idea as the session_token_login example). Device registration: session token login requires deviceToken + privateKey for this host in session storage. Use register-device (or a prior password login in @@ -171,9 +180,6 @@ export const loginCommand: CliCommandDefinition = { KEEPER_PASSWORD Password for non-interactive login (no session token). KEEPER_SESSION_TOKEN Session token when not passed as a flag. KEEPER_HOST Optional vault host / region (also: keeper-host attribute).`, - keeperSdk: ` Uses KeeperVault.login or loginWithSessionToken, then sync. resumeSession and - clone-code flows exist in the SDK but are not exposed in this CLI yet.`, - appendVaultSurface: true, }, run: (host, parsed) => runLoginCommand(host, parsed), } diff --git a/KeeperSdk/src/cli/commands/logout.ts b/KeeperSdk/src/cli/commands/logout.ts index 770527d7..aa70924a 100644 --- a/KeeperSdk/src/cli/commands/logout.ts +++ b/KeeperSdk/src/cli/commands/logout.ts @@ -32,9 +32,8 @@ export const logoutCommand: CliCommandDefinition = { help: { title: 'logout — end the current Keeper session', synopsis: ' logout', - description: ' Calls KeeperVault.logout when a session exists.', + description: ' Ends the current session if one exists.', options: ' None.', - appendVaultSurface: true, }, run: (host, parsed) => runLogoutCommand(host, parsed), } diff --git a/KeeperSdk/src/cli/commands/records.ts b/KeeperSdk/src/cli/commands/records.ts index d7b5807d..661b9da3 100644 --- a/KeeperSdk/src/cli/commands/records.ts +++ b/KeeperSdk/src/cli/commands/records.ts @@ -17,10 +17,6 @@ export const recordsCommand: CliCommandDefinition = { description: ' Runs sync, then prints a table of record_uid and title for each record.', arguments: ' list Optional; default behavior is to list. Other subcommands may be added later.', options: ' --help, -h Show this help.', - keeperSdk: ` Maps to KeeperVault.sync(), getRecords(), getRecordTitle(). - Related APIs you can extend in this CLI later: findRecords, getRecordsByType, - addRecord, updateRecord, deleteRecord, shareRecord, getRecordHistory, …`, - appendVaultSurface: true, }, async run(host, parsed) { if (wantsCliHelp(parsed)) { diff --git a/KeeperSdk/src/cli/commands/registerDevice.ts b/KeeperSdk/src/cli/commands/registerDevice.ts index 64596395..b7f2d5c8 100644 --- a/KeeperSdk/src/cli/commands/registerDevice.ts +++ b/KeeperSdk/src/cli/commands/registerDevice.ts @@ -24,8 +24,8 @@ export const registerDeviceCommand: CliCommandDefinition = { title: 'register-device — store device token and private key for session-token login', synopsis: ' register-device --device-token|--dt B64 --private-key|--pk B64 [--username|--user U]', - description: ` Calls KeeperVault.registerDevice to save device credentials for the current - host in this shell’s in-memory session. After this, you can run: + description: ` Saves device credentials for the current host in this shell’s in-memory + session. After this, you can run: login --username YOU --session-token TOKEN @@ -40,8 +40,6 @@ export const registerDeviceCommand: CliCommandDefinition = { environment: ` REGISTER_DEVICE_TOKEN Same as --device-token when flag omitted. REGISTER_DEVICE_PRIVATE_KEY Same as --private-key when flag omitted. KEEPER_HOST Same as other keeper commands.`, - keeperSdk: ' KeeperVault.registerDevice(deviceToken, privateKey, { username? })', - appendVaultSurface: true, }, async run(host, parsed) { if (wantsCliHelp(parsed)) { diff --git a/KeeperSdk/src/cli/commands/restoreSession.ts b/KeeperSdk/src/cli/commands/restoreSession.ts index cf8976cc..755280fa 100644 --- a/KeeperSdk/src/cli/commands/restoreSession.ts +++ b/KeeperSdk/src/cli/commands/restoreSession.ts @@ -146,13 +146,13 @@ export const restoreSessionCommand: CliCommandDefinition = { title: 'restore-session — restore SessionParams from extension / vault export', synopsis: ` restore-session --from-json session.json restore-session --session-token TOKEN --username U --account-uid B64 …`, - description: ` Loads a full keeperapi SessionParams snapshot and calls Auth.continueSession() - (same path as the browser extension after login). Use this when you have - accountUid, clientKey, dataKey, keys, sessionToken, username, etc. from - extension storage — not deviceToken/device private key. + description: ` Loads a full SessionParams snapshot and resumes the session (same path as + the browser extension after login). Use this when you have accountUid, + clientKey, dataKey, keys, sessionToken, username, etc. from extension storage + — deviceToken/device private key are not part of this payload. Provide parameters either as one JSON object (--from-json) or as flags / env. - Binary fields are base64 or base64url (same as normal64Bytes in keeperapi).`, + Binary fields are base64 or base64url.`, options: ` --from-json Inline JSON (object or JSON-stringified object), or a file path The entire remainder of the command line is passed to JSON.parse (then file read if needed). --account-uid, --client-key, --data-key, --ecc-private-key, --ecc-public-key @@ -168,7 +168,6 @@ export const restoreSessionCommand: CliCommandDefinition = { RESTORE_SESSION_ACCOUNT_UID Per-field overrides (see --help flags) RESTORE_SESSION_SESSION_TOKEN … (RESTORE_SESSION_ for each field above)`, - keeperSdk: ' KeeperVault.restoreSession(input) → SessionManager.saveSessionParameters + Auth.continueSession', note: ' sessionToken expires; region must match keeper-host / KEEPER_HOST.', }, async run(host, parsed) { diff --git a/KeeperSdk/src/cli/commands/sync.ts b/KeeperSdk/src/cli/commands/sync.ts index 0f0d91b8..f13284f3 100644 --- a/KeeperSdk/src/cli/commands/sync.ts +++ b/KeeperSdk/src/cli/commands/sync.ts @@ -37,12 +37,10 @@ export const syncCommand: CliCommandDefinition = { help: { title: 'sync — download vault data (syncDown)', synopsis: ' sync', - description: ` Calls KeeperVault.sync() → keeperapi syncDown to pull records, folders, and - related vault data into local storage. Requires an active session (login or restore-session).`, + description: ` Pulls records, folders, and related vault data into local storage. + Requires an active session (login or restore-session).`, options: ' --help, -h Show this help.', - keeperSdk: ' KeeperVault.sync() → syncDown({ auth, storage })', seeAlso: ' restore-session --sync, records list, folders list', - appendVaultSurface: true, }, async run(host, parsed) { if (wantsCliHelp(parsed)) { diff --git a/KeeperSdk/src/cli/help.ts b/KeeperSdk/src/cli/help.ts index b52611d3..6ebfeb26 100644 --- a/KeeperSdk/src/cli/help.ts +++ b/KeeperSdk/src/cli/help.ts @@ -1,5 +1,4 @@ import type { CliCommandDefinition, CliHelpDoc } from './types' -import { KEEPER_VAULT_SURFACE } from './vaultSurface' const SECTION_ORDER: (keyof CliHelpDoc)[] = [ 'synopsis', @@ -7,7 +6,7 @@ const SECTION_ORDER: (keyof CliHelpDoc)[] = [ 'arguments', 'options', 'environment', - 'keeperSdk', + 'examples', 'seeAlso', 'note', ] @@ -18,7 +17,7 @@ const SECTION_LABELS: Partial> = { arguments: 'ARGUMENTS', options: 'OPTIONS', environment: 'ENVIRONMENT', - keeperSdk: 'KEEPER SDK', + examples: 'EXAMPLES', seeAlso: 'SEE ALSO', note: 'NOTE', } @@ -35,10 +34,6 @@ export function formatDetailedHelp(doc: CliHelpDoc): string { } parts.push(body.trim()) } - if (doc.appendVaultSurface) { - parts.push('') - parts.push(KEEPER_VAULT_SURFACE) - } return `${parts.join('\n')}\n` } @@ -67,11 +62,7 @@ export function formatAllCommandsSummary(commands: readonly CliCommandDefinition for (const c of sorted) { out += ` ${c.name.padEnd(w)} ${c.description}\n` } - out += - '\nOptions use GNU-style syntax: `--name`, `--name=value`, short flags (`-p` or `-rf` when each letter is a switch), and `--` ends options.\n' - out += 'Quoted arguments and `\\` escapes are supported.\n\n' - out += - 'Run `help ` for a short summary, or `command --help` / `command -h` for full documentation.\n' + out += '\nRun ` --help` (or `-h`) for details on a specific command.\n' return out } diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts index a7ca6fff..ee696090 100644 --- a/KeeperSdk/src/cli/index.ts +++ b/KeeperSdk/src/cli/index.ts @@ -58,8 +58,6 @@ export function getDetailedHelpPage(name: string): string | null { return getDetailedHelpPageForRegistry(listCliCommands(), name) } -export { KEEPER_VAULT_SURFACE } from './vaultSurface' - export { registerCliCommand, registerCliAlias, @@ -73,6 +71,9 @@ export { export { dispatchKeeperCli, dispatchCliLine } from './dispatch' +export { KeeperCliParser, createKeeperCliParser } from './parser' +export type { KeeperCliParserOptions } from './parser' + export { runLoginCommand, loginWithCredentials, diff --git a/KeeperSdk/src/cli/parser.ts b/KeeperSdk/src/cli/parser.ts new file mode 100644 index 00000000..2f855a0c --- /dev/null +++ b/KeeperSdk/src/cli/parser.ts @@ -0,0 +1,199 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from './types' +import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' +import { extractFromJsonFlagValue } from './jsonArg' +import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' +import { formatAllCommandsSummary, formatDetailedHelpForCommand, formatShortCommandSummary } from './help' + +export type KeeperCliParserOptions = { + /** Program name used in usage strings (default `keeper`). */ + prog?: string + /** One-line description shown above the command list. */ + description?: string + /** Footer printed after the auto-generated command list. */ + epilog?: string +} + +/** + * Self-contained CLI parser modelled on Python `argparse.ArgumentParser` + `add_subparsers()`. + * + * Add commands once, then call {@link parse} to dispatch a CLI line. `--help` (or `-h`) at the + * top level lists every registered subcommand; ` --help` prints that command's full + * help page. The parser owns its own command set so multiple parsers can coexist (useful for + * embedding subsets of the CLI in tools, tests, or scoped UIs). + */ +export class KeeperCliParser { + private readonly prog: string + private readonly description: string + private readonly epilog?: string + private readonly commands = new Map() + private readonly aliases = new Map() + + constructor(options: KeeperCliParserOptions = {}) { + this.prog = options.prog ?? 'keeper' + this.description = options.description ?? '' + this.epilog = options.epilog + } + + /** Register one command (Commander/argparse `add_parser` equivalent). Returns `this` for chaining. */ + addCommand(def: CliCommandDefinition): this { + const key = def.name.toLowerCase() + this.commands.set(key, def) + if (def.aliases) { + for (const alias of def.aliases) { + this.aliases.set(alias.toLowerCase(), key) + } + } + return this + } + + /** Register multiple commands at once. */ + addCommands(defs: Iterable): this { + for (const def of defs) this.addCommand(def) + return this + } + + /** All registered commands, ordered by `order` then name (same rule as the global registry). */ + list(): CliCommandDefinition[] { + return [...this.commands.values()].sort((a, b) => { + const oa = a.order ?? 500 + const ob = b.order ?? 500 + if (oa !== ob) return oa - ob + return a.name.localeCompare(b.name) + }) + } + + /** All registered command names, in `list()` order. */ + listNames(): string[] { + return this.list().map((c) => c.name) + } + + /** Resolve a name (or alias) to a command definition. */ + resolve(name: string): CliCommandDefinition | undefined { + const key = name.toLowerCase() + if (this.commands.has(key)) return this.commands.get(key) + const target = this.aliases.get(key) + return target ? this.commands.get(target) : undefined + } + + /** Top-level help text: parser description + every registered subcommand. */ + formatHelp(): string { + const header = this.description ? `${this.prog} — ${this.description}\n\n` : '' + const body = formatAllCommandsSummary(this.list()) + const footer = this.epilog ? `\n${this.epilog}\n` : '' + return header + body + footer + } + + /** Full help page for one subcommand (same content as ` --help`). */ + formatCommandHelp(name: string): string | null { + const def = this.resolve(name) + return def ? formatDetailedHelpForCommand(def) : null + } + + /** Short summary for one subcommand (single line + usage). */ + formatCommandSummary(name: string): string | null { + const def = this.resolve(name) + return def ? formatShortCommandSummary(def) : null + } + + /** + * Parse and dispatch a CLI line. + * + * Behaviour: + * - empty / whitespace → top-level help (no error) + * - `--help` / `-h` / `help` → top-level help + * - `--help ` / `help ` → that command's help page + * - ` --help` / ` -h` → that command's help page (handled before `def.run`) + * - ` [args]` → run `def.run(host, parsed)` + */ + async parse(line: string | readonly string[], host: KeeperCliHost): Promise { + const { tokens, raw } = normalizeInput(line) + if (tokens.length === 0) { + return ok(this.formatHelp()) + } + + const first = tokens[0] + const rest = tokens.slice(1) + + if (isHelpToken(first)) { + const sub = rest[0] + if (!sub) return ok(this.formatHelp()) + const page = this.formatCommandHelp(sub) + if (page) return ok(page) + return err(`${this.prog}: unknown command: ${sub}\nTry: ${this.prog} --help\n`) + } + + const def = this.resolve(first) + if (!def) { + return err(`${this.prog}: unknown command: ${first}\nTry: ${this.prog} --help\n`) + } + + let parsed: ParsedCli + if (def.name === 'restore-session') { + const json = extractFromJsonFlagValue(raw, 'from-json', RESTORE_SESSION_TRAILING_OPTS) + parsed = parseCliArgs(rest) + if (json) parsed.opts.set('from-json', json) + } else { + parsed = parseCliArgs(rest) + } + + if (wantsCliHelp(parsed)) { + return ok(formatDetailedHelpForCommand(def)) + } + return def.run(host, parsed) + } +} + +/** Build a parser pre-loaded with the SDK's built-in commands (login, records, folders, …). */ +export function createKeeperCliParser(options: KeeperCliParserOptions = {}): KeeperCliParser { + const parser = new KeeperCliParser(options) + void loadBuiltinsInto(parser) + return parser +} + +function loadBuiltinsInto(parser: KeeperCliParser): void { + const { foldersCommand } = require('./commands/folders') as typeof import('./commands/folders') + const { helpCommand } = require('./commands/help') as typeof import('./commands/help') + const { loginCommand } = require('./commands/login') as typeof import('./commands/login') + const { logoutCommand } = require('./commands/logout') as typeof import('./commands/logout') + const { recordsCommand } = require('./commands/records') as typeof import('./commands/records') + const { + registerDeviceCommand, + } = require('./commands/registerDevice') as typeof import('./commands/registerDevice') + const { + restoreSessionCommand, + } = require('./commands/restoreSession') as typeof import('./commands/restoreSession') + const { syncCommand } = require('./commands/sync') as typeof import('./commands/sync') + + parser.addCommands([ + helpCommand, + loginCommand, + registerDeviceCommand, + restoreSessionCommand, + syncCommand, + recordsCommand, + foldersCommand, + logoutCommand, + ]) +} + +function normalizeInput(line: string | readonly string[]): { tokens: string[]; raw: string } { + if (typeof line === 'string') { + const trimmed = line.trim() + return { tokens: trimmed ? tokenizeArguments(trimmed) : [], raw: trimmed } + } + const tokens = [...line].filter((t) => t.length > 0) + return { tokens, raw: tokens.join(' ') } +} + +function isHelpToken(token: string): boolean { + const t = token.toLowerCase() + return t === '--help' || t === '-h' || t === 'help' +} + +function ok(out: string): CliResult { + return { code: 0, out, err: '' } +} + +function err(message: string): CliResult { + return { code: 1, out: '', err: message } +} diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts index 923afaae..b4868464 100644 --- a/KeeperSdk/src/cli/types.ts +++ b/KeeperSdk/src/cli/types.ts @@ -1,5 +1,10 @@ import type { DRecord, DSharedFolder, SyncResult } from '@keeper-security/keeperapi' import type { SessionRestoreInput } from '../auth/sessionRestore' +import type { ChangeDirectoryResult } from '../folders/changeDirectory' +import type { FolderTreeBuildOptions } from '../folders/folderTree' +import type { GetFolderOptions, GetFolderResult } from '../folders/getFolder' +import type { ListFolderOptions, ListFolderResult } from '../folders/listFolder' +import type { MkdirOptions } from '../folders/addFolder' export type CliResult = { code: number @@ -26,6 +31,14 @@ export type KeeperCliVault = { getSharedFolders(): DSharedFolder[] registerDevice(deviceToken: string, privateKey: string, options?: { username?: string }): Promise restoreSession(input: SessionRestoreInput): Promise + /** Folder navigation. Optional so existing hosts keep compiling; commands check at runtime. */ + listFolder?: (options?: ListFolderOptions) => Promise + tree?: (options?: FolderTreeBuildOptions) => Promise + changeDirectory?: (path: string) => Promise + getCurrentFolderUid?: () => string | null + getWorkingFolderDisplayName?: () => string + getFolder?: (uidOrName: string, options?: GetFolderOptions) => Promise + mkdir?: (path: string, options?: MkdirOptions) => Promise<{ folderUid: string; success: boolean; message?: string }> } /** Host adapter (browser shell, Node Commander, tests). */ @@ -44,11 +57,9 @@ export type CliHelpDoc = { arguments?: string options?: string environment?: string - keeperSdk?: string + examples?: string seeAlso?: string note?: string - /** Append standard KeeperVault API overview (login, records, folders, …). */ - appendVaultSurface?: boolean } export type CliCommandDefinition = { diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 4a886277..28f5cc33 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -299,6 +299,10 @@ export class KeeperVault { /** * Restore an existing Keeper session from extension-style {@link SessionRestoreInput} * (maps to keeperapi `SessionParams` + `continueSession`). Does not run `loginV3` or require device keys. + * + * After restoring local state, this performs a lightweight server roundtrip + * (`account_summary`) to confirm the token is still valid. A 401 / expired + * token surfaces here instead of much later from `sync()`. */ public async restoreSession(input: SessionRestoreInput): Promise { const params = toSessionParams(input) @@ -315,6 +319,21 @@ export class KeeperVault { ) } + try { + await this.auth.loadAccountSummary() + } catch (err) { + const code = extractResultCode(err) + const msg = extractErrorMessage(err) + this.disconnect() + const isExpired = code === ResultCodes.SESSION_TOKEN_EXPIRED + throw new KeeperSdkError( + isExpired + ? `Session token rejected by server (${code}): ${msg}. Re-export session JSON and try again.` + : `Session restore failed: ${msg}`, + code ?? ResultCodes.SESSION_TOKEN_EXPIRED + ) + } + this.synced = false this.log.info(`Session restored for ${params.username}`) } diff --git a/examples/sdk_example/src/utils/shellCliHost.ts b/examples/sdk_example/src/utils/shellCliHost.ts index 515a773d..327a1f2b 100644 --- a/examples/sdk_example/src/utils/shellCliHost.ts +++ b/examples/sdk_example/src/utils/shellCliHost.ts @@ -70,6 +70,13 @@ function asCliVault(v: VaultInstance): KeeperCliVault { getSharedFolders: () => v.getSharedFolders(), registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), restoreSession: (input) => v.restoreSession(input), + listFolder: (options) => v.listFolder(options), + tree: (options) => v.tree(options), + changeDirectory: (path) => v.changeDirectory(path), + getCurrentFolderUid: () => v.getCurrentFolderUid(), + getWorkingFolderDisplayName: () => v.getWorkingFolderDisplayName(), + getFolder: (uidOrName, options) => v.getFolder(uidOrName, options), + mkdir: (path, options) => v.mkdir(path, options), } } diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts index 05eca4aa..1c857162 100644 --- a/shellcomponent/src/cli/keeperCliHost.ts +++ b/shellcomponent/src/cli/keeperCliHost.ts @@ -87,6 +87,13 @@ function asCliVault(v: VaultInstance): KeeperCliVault { getSharedFolders: () => v.getSharedFolders(), registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), restoreSession: (input) => v.restoreSession(input), + listFolder: (options) => v.listFolder(options), + tree: (options) => v.tree(options), + changeDirectory: (path) => v.changeDirectory(path), + getCurrentFolderUid: () => v.getCurrentFolderUid(), + getWorkingFolderDisplayName: () => v.getWorkingFolderDisplayName(), + getFolder: (uidOrName, options) => v.getFolder(uidOrName, options), + mkdir: (path, options) => v.mkdir(path, options), }; } From 40689c7dff13c0cefb4a7b8201bbbf9c97746cf0 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Wed, 27 May 2026 09:51:47 +0530 Subject: [PATCH 08/23] refactored to reduce bloat - iter 1 --- KeeperSdk/src/cli/commands/login.ts | 9 +----- KeeperSdk/src/cli/commands/restoreSession.ts | 2 +- KeeperSdk/src/cli/parser.ts | 32 ++----------------- KeeperSdk/src/cli/types.ts | 10 +++--- KeeperSdk/src/vault/KeeperVault.ts | 9 ++---- examples/sdk_example/README.md | 13 +++----- .../src/records/list_records_shell_cli.ts | 7 +--- .../src/utils/inMemoryConfigLoader.ts | 2 +- .../sdk_example/src/utils/shellCliHost.ts | 6 ++-- .../sdk_example/src/utils/shellCliRestore.ts | 7 ++-- shellcomponent/README.md | 15 +-------- .../src/dev/installKeeperSameOriginProxy.ts | 5 +-- shellcomponent/src/dev/keeperProxyHosts.ts | 2 +- .../vite/keeperSameOriginProxyPlugin.ts | 5 +-- 14 files changed, 27 insertions(+), 97 deletions(-) diff --git a/KeeperSdk/src/cli/commands/login.ts b/KeeperSdk/src/cli/commands/login.ts index 4d2313f1..25d9c294 100644 --- a/KeeperSdk/src/cli/commands/login.ts +++ b/KeeperSdk/src/cli/commands/login.ts @@ -116,14 +116,7 @@ export async function loginWithSessionToken( } } -/** - * Returns success if a session is active. Otherwise emits a plain - * "not logged in" error — no login flags or prompts. Commands that want - * env-based auto-login should call runLoginCommand explicitly. - * - * Auto-login is preserved only when KEEPER_USERNAME is present in env; - * runLoginCommand then handles password/session-token resolution. - */ +/** Pass-through if logged in; auto-login when `KEEPER_USERNAME` is set; otherwise "not logged in". */ export async function ensureLoggedIn(host: KeeperCliHost): Promise { if (host.getVault().isLoggedIn) { return { code: 0, out: '', err: '' } diff --git a/KeeperSdk/src/cli/commands/restoreSession.ts b/KeeperSdk/src/cli/commands/restoreSession.ts index 755280fa..7e416d59 100644 --- a/KeeperSdk/src/cli/commands/restoreSession.ts +++ b/KeeperSdk/src/cli/commands/restoreSession.ts @@ -8,7 +8,7 @@ import { getOpt, hasOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' import { formatDetailedHelpForCommand } from '../help' import { runVaultSync } from './sync' -/** Flags that may appear after `--from-json ` on the same line (stripped before parse/read). */ +/** Flags allowed to follow `--from-json ` on the same line. */ export const RESTORE_SESSION_TRAILING_OPTS = [ 'sync', 'account-uid', diff --git a/KeeperSdk/src/cli/parser.ts b/KeeperSdk/src/cli/parser.ts index 2f855a0c..82e0f584 100644 --- a/KeeperSdk/src/cli/parser.ts +++ b/KeeperSdk/src/cli/parser.ts @@ -5,22 +5,12 @@ import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' import { formatAllCommandsSummary, formatDetailedHelpForCommand, formatShortCommandSummary } from './help' export type KeeperCliParserOptions = { - /** Program name used in usage strings (default `keeper`). */ prog?: string - /** One-line description shown above the command list. */ description?: string - /** Footer printed after the auto-generated command list. */ epilog?: string } -/** - * Self-contained CLI parser modelled on Python `argparse.ArgumentParser` + `add_subparsers()`. - * - * Add commands once, then call {@link parse} to dispatch a CLI line. `--help` (or `-h`) at the - * top level lists every registered subcommand; ` --help` prints that command's full - * help page. The parser owns its own command set so multiple parsers can coexist (useful for - * embedding subsets of the CLI in tools, tests, or scoped UIs). - */ +/** Self-contained CLI parser. Register commands, then `parse()` dispatches a line. */ export class KeeperCliParser { private readonly prog: string private readonly description: string @@ -34,7 +24,6 @@ export class KeeperCliParser { this.epilog = options.epilog } - /** Register one command (Commander/argparse `add_parser` equivalent). Returns `this` for chaining. */ addCommand(def: CliCommandDefinition): this { const key = def.name.toLowerCase() this.commands.set(key, def) @@ -46,13 +35,11 @@ export class KeeperCliParser { return this } - /** Register multiple commands at once. */ addCommands(defs: Iterable): this { for (const def of defs) this.addCommand(def) return this } - /** All registered commands, ordered by `order` then name (same rule as the global registry). */ list(): CliCommandDefinition[] { return [...this.commands.values()].sort((a, b) => { const oa = a.order ?? 500 @@ -62,12 +49,10 @@ export class KeeperCliParser { }) } - /** All registered command names, in `list()` order. */ listNames(): string[] { return this.list().map((c) => c.name) } - /** Resolve a name (or alias) to a command definition. */ resolve(name: string): CliCommandDefinition | undefined { const key = name.toLowerCase() if (this.commands.has(key)) return this.commands.get(key) @@ -75,7 +60,6 @@ export class KeeperCliParser { return target ? this.commands.get(target) : undefined } - /** Top-level help text: parser description + every registered subcommand. */ formatHelp(): string { const header = this.description ? `${this.prog} — ${this.description}\n\n` : '' const body = formatAllCommandsSummary(this.list()) @@ -83,28 +67,16 @@ export class KeeperCliParser { return header + body + footer } - /** Full help page for one subcommand (same content as ` --help`). */ formatCommandHelp(name: string): string | null { const def = this.resolve(name) return def ? formatDetailedHelpForCommand(def) : null } - /** Short summary for one subcommand (single line + usage). */ formatCommandSummary(name: string): string | null { const def = this.resolve(name) return def ? formatShortCommandSummary(def) : null } - /** - * Parse and dispatch a CLI line. - * - * Behaviour: - * - empty / whitespace → top-level help (no error) - * - `--help` / `-h` / `help` → top-level help - * - `--help ` / `help ` → that command's help page - * - ` --help` / ` -h` → that command's help page (handled before `def.run`) - * - ` [args]` → run `def.run(host, parsed)` - */ async parse(line: string | readonly string[], host: KeeperCliHost): Promise { const { tokens, raw } = normalizeInput(line) if (tokens.length === 0) { @@ -143,7 +115,7 @@ export class KeeperCliParser { } } -/** Build a parser pre-loaded with the SDK's built-in commands (login, records, folders, …). */ +/** Parser pre-loaded with the SDK's built-in commands. */ export function createKeeperCliParser(options: KeeperCliParserOptions = {}): KeeperCliParser { const parser = new KeeperCliParser(options) void loadBuiltinsInto(parser) diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts index b4868464..343d2945 100644 --- a/KeeperSdk/src/cli/types.ts +++ b/KeeperSdk/src/cli/types.ts @@ -10,7 +10,7 @@ export type CliResult = { code: number out: string err: string - /** Login needs masked password from host UI (never in CLI line). */ + /** Set when the host UI must prompt for a masked password (never on the CLI line). */ needPassword?: boolean loginUsername?: string } @@ -20,7 +20,7 @@ export type ParsedCli = { opts: Map } -/** Vault surface used by CLI command handlers. */ +/** Vault surface used by CLI command handlers. Folder methods are optional — commands check at runtime. */ export type KeeperCliVault = { readonly isLoggedIn: boolean login(username: string, password: string): Promise @@ -31,7 +31,6 @@ export type KeeperCliVault = { getSharedFolders(): DSharedFolder[] registerDevice(deviceToken: string, privateKey: string, options?: { username?: string }): Promise restoreSession(input: SessionRestoreInput): Promise - /** Folder navigation. Optional so existing hosts keep compiling; commands check at runtime. */ listFolder?: (options?: ListFolderOptions) => Promise tree?: (options?: FolderTreeBuildOptions) => Promise changeDirectory?: (path: string) => Promise @@ -41,12 +40,11 @@ export type KeeperCliVault = { mkdir?: (path: string, options?: MkdirOptions) => Promise<{ folderUid: string; success: boolean; message?: string }> } -/** Host adapter (browser shell, Node Commander, tests). */ +/** Host adapter (browser shell, Node script, tests). `readTextFile` is optional. */ export type KeeperCliHost = { getVault(): KeeperCliVault envString(name: string): string | undefined formatError(context: string, err: unknown): string - /** Read a local or remote text file (browser dev: Vite `/@fs/…` paths). */ readTextFile?: (path: string) => Promise } @@ -70,7 +68,7 @@ export type CliCommandDefinition = { aliases?: readonly string[] subcommands?: readonly string[] flagOptions?: readonly string[] - /** If set, unknown options are rejected (excluding help). */ + /** When set, options outside this set are rejected (`--help` / `-h` always allowed). */ allowedOptions?: ReadonlySet help: CliHelpDoc run: (host: KeeperCliHost, parsed: ParsedCli) => Promise diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 28f5cc33..55259f19 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -297,12 +297,9 @@ export class KeeperVault { } /** - * Restore an existing Keeper session from extension-style {@link SessionRestoreInput} - * (maps to keeperapi `SessionParams` + `continueSession`). Does not run `loginV3` or require device keys. - * - * After restoring local state, this performs a lightweight server roundtrip - * (`account_summary`) to confirm the token is still valid. A 401 / expired - * token surfaces here instead of much later from `sync()`. + * Resume a session from extension-exported {@link SessionRestoreInput}. + * Verifies the token with a lightweight server call so an expired session + * fails here, not later from `sync()`. */ public async restoreSession(input: SessionRestoreInput): Promise { const params = toSessionParams(input) diff --git a/examples/sdk_example/README.md b/examples/sdk_example/README.md index 996c12f0..45455b71 100644 --- a/examples/sdk_example/README.md +++ b/examples/sdk_example/README.md @@ -68,19 +68,16 @@ npm run records:get Most examples will log in automatically using persistent login (if configured) or prompt for credentials. After authentication, follow the interactive prompts. -**Restore-session flag** on `records:list` (requires `--from-json`; no default path). Uses the same SDK CLI dispatch as shellcomponent (`dispatchCliLine` → `restore-session`): +**Restore-session** on `records:list` (requires `--from-json`): ```bash -npm run records:list -- --restore-session --from-json /path/to/session.json -npm run records:list -- --restore-session --from-json /path/to/session.json --host keepersecurity.eu -npm run records:list -- --restore-session --from-json /path/to/session.json --no-sync +npm run records:list -- --restore-session --from-json /path/to/session.json [--host keepersecurity.eu] [--no-sync] ``` -**Shell-parity debug** — restore and list only through CLI commands (closest match to typing in keeper-shell): +**Shell-parity debug** — same dispatch path as `` (uses `dispatchCliLine` → `restore-session`): ```bash -npm run records:list:shell-cli -- --from-json /path/to/session.json -npm run records:list:shell-cli -- --from-json /path/to/session.json --host keepersecurity.eu +npm run records:list:shell-cli -- --from-json /path/to/session.json [--host keepersecurity.eu] ``` -If Node succeeds but the browser shell fails, the difference is likely host I/O (`readTextFile` / Vite `/@fs`), CORS, or region (`keeper-host` / `KEEPER_HOST`), not `KeeperVault.restoreSession` itself. +If Node works but the browser shell fails, suspect host I/O (`readTextFile` / Vite `/@fs`), CORS, or region — not `KeeperVault.restoreSession` itself. diff --git a/examples/sdk_example/src/records/list_records_shell_cli.ts b/examples/sdk_example/src/records/list_records_shell_cli.ts index 53536047..a25a095b 100644 --- a/examples/sdk_example/src/records/list_records_shell_cli.ts +++ b/examples/sdk_example/src/records/list_records_shell_cli.ts @@ -3,12 +3,7 @@ import { runExample } from '../utils/runner' import { assertRestoreCliArgs, parseRestoreCliArgs, requireSessionJsonPath } from '../utils/restoreAuth' import { listRecordsViaShellCli, loginViaShellCliRestoreSession } from '../utils/shellCliRestore' -/** - * Debug script: restore + list entirely through SDK CLI dispatch (mirrors shellcomponent). - * - * npm run records:list:shell-cli -- --from-json /path/to/session.json - * npm run records:list:shell-cli -- --from-json /path/to/session.json --host keepersecurity.eu - */ +// npm run records:list:shell-cli -- --from-json /path/to/session.json [--host keepersecurity.eu] async function main() { const cli = parseRestoreCliArgs() if (!cli.restoreSession && !cli.jsonPath) { diff --git a/examples/sdk_example/src/utils/inMemoryConfigLoader.ts b/examples/sdk_example/src/utils/inMemoryConfigLoader.ts index 8a0c8422..efcaa01e 100644 --- a/examples/sdk_example/src/utils/inMemoryConfigLoader.ts +++ b/examples/sdk_example/src/utils/inMemoryConfigLoader.ts @@ -1,6 +1,6 @@ import type { ConfigLoader, KeeperJsonConfig } from '@keeper-security/keeper-sdk-javascript' -/** In-memory session/device storage (same role as shellcomponent's InMemoryConfigLoader). */ +/** Process-lifetime session/device storage (no disk). */ export class InMemoryConfigLoader implements ConfigLoader { public readonly configDir = '' diff --git a/examples/sdk_example/src/utils/shellCliHost.ts b/examples/sdk_example/src/utils/shellCliHost.ts index 327a1f2b..9858296c 100644 --- a/examples/sdk_example/src/utils/shellCliHost.ts +++ b/examples/sdk_example/src/utils/shellCliHost.ts @@ -13,7 +13,7 @@ type VaultInstance = KeeperVault let keeperHost: string | undefined let vault: VaultInstance | null = null -/** Optional region/host override (shell: keeper-host / KEEPER_HOST). */ +/** Override region/host (mirrors shell's `keeper-host` / `KEEPER_HOST`). */ export function setExampleKeeperHost(host: string | undefined): void { const trimmed = host?.trim() keeperHost = trimmed || undefined @@ -44,7 +44,7 @@ function getVault(): VaultInstance { return vault } -/** Underlying vault instance (for cleanup() after CLI dispatch). */ +/** Underlying vault (e.g. for `cleanup()` after CLI dispatch). */ export function getExampleKeeperVault(): VaultInstance { return getVault() } @@ -98,7 +98,7 @@ function envString(name: string): string | undefined { return typeof v === 'string' && v.length > 0 ? v : undefined } -/** Node adapter matching shellcomponent/src/cli/keeperCliHost.ts (fs instead of Vite fetch). */ +/** Node `KeeperCliHost` (fs file-reads, no browser proxy). */ export const exampleShellCliHost: KeeperCliHost = { getVault: () => asCliVault(getVault()), envString, diff --git a/examples/sdk_example/src/utils/shellCliRestore.ts b/examples/sdk_example/src/utils/shellCliRestore.ts index bb0017ee..bb128678 100644 --- a/examples/sdk_example/src/utils/shellCliRestore.ts +++ b/examples/sdk_example/src/utils/shellCliRestore.ts @@ -15,10 +15,9 @@ import { } from './shellCliHost' export type ShellCliRestoreOptions = { - /** Resolved or raw path to session JSON (caller should validate). */ jsonPath: string host?: string - /** Run `restore-session --sync` (default true). */ + /** Defaults to true. */ sync?: boolean } @@ -43,9 +42,7 @@ function throwOnCliFailure(label: string, result: CliResult): void { ) } -/** - * Authenticate via SDK `restore-session` CLI (same dispatch path as shellcomponent). - */ +/** Authenticate via SDK `restore-session` CLI (same dispatch path as shellcomponent). */ export async function loginViaShellCliRestoreSession( options: ShellCliRestoreOptions ): Promise { diff --git a/shellcomponent/README.md b/shellcomponent/README.md index c8f5576d..b78b83fe 100644 --- a/shellcomponent/README.md +++ b/shellcomponent/README.md @@ -100,28 +100,15 @@ npm run dev Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell. -### Same-origin dev (in-browser SDK, no separate Node API) - -The dev page runs the **Keeper SDK in the browser** (no **`remote`** / **`api-base`**). Cross-origin calls to `keepersecurity.com` would fail CORS from `localhost`, so dev wires a **same-origin proxy**: - -1. **`installKeeperSameOriginProxy`** (in `dev-bootstrap.ts`) rewrites `fetch` / `WebSocket` URLs to - `http://localhost:5175/__keeper//…` and `ws://localhost:5175/__keeper-wss//…` -2. **`keeperSameOriginProxyPlugin`** (Vite) forwards those paths to the real Keeper HTTPS/WSS hosts. - -Only **`npm run dev`** is required. Example: +The dev page runs the **Keeper SDK in the browser** (no `remote` / `api-base`). To avoid CORS, dev wires a **same-origin proxy** — see [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md). Only `npm run dev` is needed. ```text restore-session --from-json /dev/keeper-session.json --sync records list ``` -Use an absolute path to repo `conf.json` if you prefer: -`restore-session --from-json /Users/you/.../keeper-sdk-javascript/conf.json --sync` - Set region on the element when needed: ``. -**Production:** your deployed origin must be allowed by Keeper’s CORS policy, or you host a trusted API and use **`remote`** + **`api-base`**. The Vite proxy is for **local dev only**. - ```bash npm run build ``` diff --git a/shellcomponent/src/dev/installKeeperSameOriginProxy.ts b/shellcomponent/src/dev/installKeeperSameOriginProxy.ts index bf12ea5a..1e758a4e 100644 --- a/shellcomponent/src/dev/installKeeperSameOriginProxy.ts +++ b/shellcomponent/src/dev/installKeeperSameOriginProxy.ts @@ -30,10 +30,7 @@ function rewriteKeeperUrl(url: string): string { } } -/** - * Dev-only: route Keeper REST/WebSocket through the Vite origin (`/__keeper/…`, `/__keeper-wss/…`). - * SDK and CLI stay in the browser; Vite proxies to Keeper (same-origin from the page’s view). - */ +/** Dev: rewrite Keeper fetch/WebSocket URLs to the same-origin Vite proxy prefixes. */ export function installKeeperSameOriginProxy(): void { if (typeof window === "undefined") return; diff --git a/shellcomponent/src/dev/keeperProxyHosts.ts b/shellcomponent/src/dev/keeperProxyHosts.ts index fbb1ced3..38ba2d8c 100644 --- a/shellcomponent/src/dev/keeperProxyHosts.ts +++ b/shellcomponent/src/dev/keeperProxyHosts.ts @@ -1,4 +1,4 @@ -/** Dev-only: allow proxying to known Keeper infrastructure hosts. */ +/** Allowlist of Keeper infra hosts the dev proxy may forward to. */ export function isAllowedKeeperProxyHost(host: string): boolean { if (!host || host.includes("..") || host.includes("/")) return false; if (/^push\.services\./.test(host)) { diff --git a/shellcomponent/vite/keeperSameOriginProxyPlugin.ts b/shellcomponent/vite/keeperSameOriginProxyPlugin.ts index 56957a0a..73fe160b 100644 --- a/shellcomponent/vite/keeperSameOriginProxyPlugin.ts +++ b/shellcomponent/vite/keeperSameOriginProxyPlugin.ts @@ -2,10 +2,7 @@ import type { Plugin } from "vite"; import httpProxy from "http-proxy"; import { isAllowedKeeperProxyHost } from "../src/dev/keeperProxyHosts.js"; -/** - * Vite dev server: forward same-origin `/__keeper//…` and `/__keeper-wss//…` - * to real Keeper HTTPS/WSS endpoints (avoids browser CORS while SDK runs in-page). - */ +/** Dev: proxy `/__keeper//…` (HTTPS) and `/__keeper-wss//…` (WSS) to Keeper. */ export function keeperSameOriginProxyPlugin(): Plugin { const proxy = httpProxy.createProxyServer({ changeOrigin: true, From 4302002ea6b8340042ee92c495204d6ce342ded0 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Thu, 28 May 2026 20:21:22 +0530 Subject: [PATCH 09/23] added support for teams, users, roles etc. moving to individual contexts for enterprise and vault is pending --- KeeperSdk/src/cli/commandHelpers.ts | 24 +++ KeeperSdk/src/cli/commands/folders.ts | 97 ++++++---- KeeperSdk/src/cli/commands/records.ts | 171 ++++++++++++++---- KeeperSdk/src/cli/commands/sharedFolders.ts | 66 +++++++ KeeperSdk/src/cli/commands/teams.ts | 99 ++++++++++ KeeperSdk/src/cli/commands/users.ts | 115 ++++++++++++ KeeperSdk/src/cli/commands/vault.ts | 56 ++++++ KeeperSdk/src/cli/index.ts | 12 ++ KeeperSdk/src/cli/parser.ts | 10 + KeeperSdk/src/cli/table.ts | 8 + KeeperSdk/src/cli/types.ts | 25 ++- KeeperSdk/src/vault/KeeperVault.ts | 10 +- .../sdk_example/src/utils/shellCliHost.ts | 11 ++ shellcomponent/README.md | 24 ++- shellcomponent/keeper-shell.d.ts | 5 + shellcomponent/src/KeeperShell.ts | 5 +- shellcomponent/src/cli/cliComplete.ts | 25 ++- shellcomponent/src/cli/cliContext.ts | 27 ++- shellcomponent/src/cli/cliDispatch.ts | 8 +- shellcomponent/src/cli/keeperCliHost.ts | 19 +- shellcomponent/src/cli/keeperCommands.ts | 5 +- shellcomponent/src/cli/mkdirCommand.ts | 40 ---- shellcomponent/src/index.ts | 6 +- shellcomponent/src/vite-env.d.ts | 10 +- 24 files changed, 748 insertions(+), 130 deletions(-) create mode 100644 KeeperSdk/src/cli/commandHelpers.ts create mode 100644 KeeperSdk/src/cli/commands/sharedFolders.ts create mode 100644 KeeperSdk/src/cli/commands/teams.ts create mode 100644 KeeperSdk/src/cli/commands/users.ts create mode 100644 KeeperSdk/src/cli/commands/vault.ts create mode 100644 KeeperSdk/src/cli/table.ts delete mode 100644 shellcomponent/src/cli/mkdirCommand.ts diff --git a/KeeperSdk/src/cli/commandHelpers.ts b/KeeperSdk/src/cli/commandHelpers.ts new file mode 100644 index 00000000..e4fa6652 --- /dev/null +++ b/KeeperSdk/src/cli/commandHelpers.ts @@ -0,0 +1,24 @@ +import type { CliResult, KeeperCliHost, KeeperCliVault } from './types' +import { ensureLoggedIn } from './commands/login' + +export async function ensureSession(host: KeeperCliHost): Promise { + const v = host.getVault() + if (v.isLoggedIn) return null + const r = await ensureLoggedIn(host) + return r.code === 0 ? null : r +} + +export function ensureCapability( + v: KeeperCliVault, + name: K, + context: string +): CliResult | null { + if (typeof v[name] !== 'function') { + return { + code: 1, + out: '', + err: `${context}: this host does not expose KeeperCliVault.${String(name)}.\n`, + } + } + return null +} diff --git a/KeeperSdk/src/cli/commands/folders.ts b/KeeperSdk/src/cli/commands/folders.ts index 90b186f9..cd840c50 100644 --- a/KeeperSdk/src/cli/commands/folders.ts +++ b/KeeperSdk/src/cli/commands/folders.ts @@ -1,43 +1,13 @@ import type { DSharedFolder } from '@keeper-security/keeperapi' -import type { CliCommandDefinition, CliResult, KeeperCliHost, KeeperCliVault, ParsedCli } from '../types' +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' import { hasOpt, wantsCliHelp } from '../parse' import { formatDetailedHelpForCommand } from '../help' -import { ensureLoggedIn } from './login' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { formatTable } from '../table' -const SUBCOMMANDS = ['list', 'tree', 'ls', 'pwd', 'cd', 'mkdir', 'get'] as const +const SUBCOMMANDS = ['list', 'tree', 'ls', 'pwd', 'cd', 'mkdir', 'rename', 'rmdir', 'get'] as const type Sub = (typeof SUBCOMMANDS)[number] -function ensureCapability( - v: KeeperCliVault, - name: K, - sub: string -): CliResult | null { - if (typeof v[name] !== 'function') { - return { - code: 1, - out: '', - err: `folders ${sub}: this host does not expose KeeperCliVault.${String(name)}.\n`, - } - } - return null -} - -async function ensureSession(host: KeeperCliHost): Promise { - const v = host.getVault() - if (v.isLoggedIn) return null - const r = await ensureLoggedIn(host) - return r.code === 0 ? null : r -} - -/** Fixed-width column formatter. Last column is left unpadded so trailing whitespace is avoided. */ -function formatTable(headers: string[], rows: string[][]): string { - if (rows.length === 0) return '' - const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))) - const fmt = (cells: string[]): string => - cells.map((s, i) => (i === cells.length - 1 ? s : (s ?? '').padEnd(widths[i]))).join(' ') - return [fmt(headers), ...rows.map(fmt)].join('\n') + '\n' -} - async function runListShared(host: KeeperCliHost): Promise { const r = await ensureSession(host) if (r) return r @@ -178,6 +148,53 @@ async function runMkdir(host: KeeperCliHost, parsed: ParsedCli): Promise { + const path = parsed.positional[1] + const newName = parsed.positional[2] + if (!path || !newName) { + return { + code: 1, + out: '', + err: 'folders rename: usage: folders rename \n', + } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'renameFolder', 'folders rename') + if (cap) return cap + try { + const res = await v.renameFolder!(path, newName) + if (!res.success) { + return { code: 1, out: '', err: `folders rename: ${res.message ?? 'failed'}\n` } + } + return { code: 0, out: `renamed ${path} → ${newName}\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`folders rename ${path}`, e) } + } +} + +async function runRmdir(host: KeeperCliHost, parsed: ParsedCli): Promise { + const pattern = parsed.positional[1] + if (!pattern) { + return { code: 1, out: '', err: 'folders rmdir: missing path. Usage: folders rmdir \n' } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'rmdir', 'folders rmdir') + if (cap) return cap + try { + const res = await v.rmdir!([pattern]) + if (!res.success) { + return { code: 1, out: '', err: `folders rmdir: ${res.message ?? 'failed'}\n` } + } + return { code: 0, out: `removed ${pattern}\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`folders rmdir ${pattern}`, e) } + } +} + async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise { const target = parsed.positional[1] if (!target) return { code: 1, out: '', err: 'folders get: missing UID or name. Usage: folders get \n' } @@ -198,8 +215,8 @@ async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + await v.sync() + const records = v.getRecords() + const rows = records.map((rec) => [recordUid(rec), getRecordTitle(rec)]) + const header = 'record_uid\ttitle\n' + const body = rows.length ? rows.join('\n') + '\n' : '(no records)\n' + return { code: 0, out: header + body, err: '' } +} + +async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise { + const target = parsed.positional[1] + if (!target) { + return { code: 1, out: '', err: 'records get: missing UID or title. Usage: records get \n' } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'findRecord', 'records get') + if (cap) return cap + await v.sync() + const record = v.findRecord!(target) + if (!record) { + return { code: 1, out: '', err: `records get: no record matching "${target}"\n` } + } + const detail = hasOpt(parsed.opts, 'detail') + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(record, null, 2) + '\n', err: '' } + } + return { code: 0, out: formatRecord(record, detail) + '\n', err: '' } +} + +async function runFind(host: KeeperCliHost, parsed: ParsedCli): Promise { + const criteria = parsed.positional[1] ?? getOpt(parsed.opts, 'pattern') + if (!criteria) { + return { + code: 1, + out: '', + err: 'records find: missing search text. Usage: records find or records find --pattern \n', + } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'findRecords', 'records find') + if (cap) return cap + await v.sync() + const matches = v.findRecords!(criteria) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(matches, null, 2) + '\n', err: '' } + } + if (matches.length === 0) { + return { code: 0, out: `(no records matched "${criteria}")\n`, err: '' } + } + const rows = matches.map((rec) => [recordUid(rec), getRecordTitle(rec)]) + return { code: 0, out: formatTable(['record_uid', 'title'], rows), err: '' } +} + +async function runShareInfo(host: KeeperCliHost, parsed: ParsedCli): Promise { + const target = parsed.positional[1] + if (!target) { + return { + code: 1, + out: '', + err: 'records share-info: missing UID or title. Usage: records share-info \n', + } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + if (!v.findRecord || !v.getRecordShareInfo) { + return { + code: 1, + out: '', + err: 'records share-info: host lacks findRecord or getRecordShareInfo.\n', + } + } + await v.sync() + const record = v.findRecord(target) + if (!record?.uid) { + return { code: 1, out: '', err: `records share-info: no record matching "${target}"\n` } + } + const info = await v.getRecordShareInfo(record.uid) + if (!info) { + return { code: 0, out: '(no share information)\n', err: '' } + } + return { code: 0, out: JSON.stringify(info, null, 2) + '\n', err: '' } +} export const recordsCommand: CliCommandDefinition = { name: 'records', order: 30, - description: 'List vault records (uid and title).', - usage: 'records [list] [--help|-h]', - subcommands: ['list'], + description: 'Vault records (list, get, find, share-info).', + usage: 'records list|get|find|share-info [args] [--detail] [--pattern] [--json] [--help|-h]', + subcommands: [...SUBCOMMANDS], + flagOptions: ['--detail', '--pattern', '--json'], help: { - title: 'records — list vault records (record UID and title)', - synopsis: ' records [list]', - description: ' Runs sync, then prints a table of record_uid and title for each record.', - arguments: ' list Optional; default behavior is to list. Other subcommands may be added later.', - options: ' --help, -h Show this help.', + title: 'records — search and inspect vault records', + synopsis: ` records [list] + records get UID|TITLE [--detail] [--json] + records find TEXT [--pattern TEXT] [--json] + records share-info UID|TITLE`, + description: ' list syncs and prints uid + title. get/find resolve by uid or title substring.', + arguments: ` list (default) Table of all records. + get One record (formatted text or --json). + find Search records by uid/title tokens. + share-info JSON share permissions for a record.`, + options: ` --detail get only: include extra fields in formatted output. + --pattern find only: same as positional search text. + --json JSON output (get, find, share-info). + --help, -h Show this help.`, + examples: ` records list + records get "My Login" --detail + records find password + records share-info abc123uid`, + seeAlso: ' folders ls, vault summary, sync', }, async run(host, parsed) { if (wantsCliHelp(parsed)) { return { code: 0, out: formatDetailedHelpForCommand(recordsCommand), err: '' } } - if (parsed.opts.size > 0) { - return { code: 1, out: '', err: 'records: unknown option (try: records --help)\n' } - } - const sub = parsed.positional[0]?.toLowerCase() - if (parsed.positional.length > 1) { - return { code: 1, out: '', err: 'Usage: records [list]\n' } - } - if (sub && sub !== 'list') { - return { code: 1, out: '', err: 'Usage: records [list]\n' } + const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub + if (!SUBCOMMANDS.includes(sub)) { + return { + code: 1, + out: '', + err: `records: unknown subcommand "${parsed.positional[0]}". Try: records --help\n`, + } } try { - const v = host.getVault() - if (!v.isLoggedIn) { - const r = await ensureLoggedIn(host) - if (r.code !== 0) return r + switch (sub) { + case 'list': + return await runList(host) + case 'get': + return await runGet(host, parsed) + case 'find': + return await runFind(host, parsed) + case 'share-info': + return await runShareInfo(host, parsed) } - await v.sync() - const records = v.getRecords() - const rows = records.map((r) => `${recordUid(r)}\t${getRecordTitle(r)}`) - const header = 'record_uid\ttitle\n' - const body = rows.length ? rows.join('\n') + '\n' : '(no records)\n' - return { code: 0, out: header + body, err: '' } } catch (e) { - return { code: 1, out: '', err: host.formatError('records', e) } + return { code: 1, out: '', err: host.formatError(`records ${sub}`, e) } } }, } diff --git a/KeeperSdk/src/cli/commands/sharedFolders.ts b/KeeperSdk/src/cli/commands/sharedFolders.ts new file mode 100644 index 00000000..77324c3e --- /dev/null +++ b/KeeperSdk/src/cli/commands/sharedFolders.ts @@ -0,0 +1,66 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { getOpt, hasOpt, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { formatSharedFoldersTable, renderSharedFoldersAsciiTable } from '../../sharedFolders/listSharedFolders' + +async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listSharedFolders', 'shared-folders list') + if (cap) return cap + await v.sync!() + const pattern = getOpt(parsed.opts, 'pattern') ?? null + const verbose = hasOpt(parsed.opts, 'verbose') + const rows = v.listSharedFolders!({ pattern, verbose, includeDetails: verbose }) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } + } + if (rows.length === 0) { + return { + code: 0, + out: pattern ? `(no shared folders matched "${pattern}")\n` : '(no shared folders)\n', + err: '', + } + } + const table = formatSharedFoldersTable(rows, { verbose }) + return { code: 0, out: renderSharedFoldersAsciiTable(table) + '\n', err: '' } +} + +export const sharedFoldersCommand: CliCommandDefinition = { + name: 'shared-folders', + order: 32, + description: 'List shared folders (with optional counts).', + usage: 'shared-folders [list] [--pattern P] [--verbose] [--json] [--help|-h]', + subcommands: ['list'], + flagOptions: ['--pattern', '--verbose', '--json'], + help: { + title: 'shared-folders — list shared folders in the vault', + synopsis: ' shared-folders [list] [--pattern P] [--verbose] [--json]', + description: + ' Unlike `folders list` (uid + name only), this command can include team/user/record counts with --verbose.', + arguments: ' list (default) List shared folders.', + options: ` --pattern Filter by name or uid substring. + --verbose Include team/user/record counts and default permissions. + --json Emit JSON. + --help, -h Show this help.`, + examples: ` shared-folders + shared-folders list --pattern marketing --verbose`, + seeAlso: ' folders list, folders ls, sync', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(sharedFoldersCommand), err: '' } + } + const sub = parsed.positional[0]?.toLowerCase() + if (sub && sub !== 'list') { + return { code: 1, out: '', err: 'Usage: shared-folders [list]\n' } + } + try { + return await runList(host, parsed) + } catch (e) { + return { code: 1, out: '', err: host.formatError('shared-folders', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/teams.ts b/KeeperSdk/src/cli/commands/teams.ts new file mode 100644 index 00000000..64e435d7 --- /dev/null +++ b/KeeperSdk/src/cli/commands/teams.ts @@ -0,0 +1,99 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, KeeperCliVault, ParsedCli } from '../types' +import { getOpt, hasOpt, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { formatTeamsTable, renderTeamsAsciiTable } from '../../teams/listTeams' +import { formatTeamView, teamViewTable } from '../../teams/viewTeam' + +const SUBCOMMANDS = ['list', 'view'] as const +type Sub = (typeof SUBCOMMANDS)[number] + +async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listTeams', 'teams list') + if (cap) return cap + await v.sync!() + const pattern = getOpt(parsed.opts, 'pattern') ?? null + const rows = await v.listTeams!({ pattern }) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } + } + if (rows.length === 0) { + return { + code: 0, + out: pattern ? `(no teams matched "${pattern}")\n` : '(no teams)\n', + err: '', + } + } + const table = formatTeamsTable(rows) + return { code: 0, out: renderTeamsAsciiTable(table) + '\n', err: '' } +} + +async function runView(host: KeeperCliHost, parsed: ParsedCli): Promise { + const id = parsed.positional[1] + if (!id) { + return { code: 1, out: '', err: 'teams view: missing team name or UID. Usage: teams view \n' } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'viewTeam', 'teams view') + if (cap) return cap + const view = await v.viewTeam!(id) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(view, null, 2) + '\n', err: '' } + } + const formatted = formatTeamView(view, { verbose: hasOpt(parsed.opts, 'verbose') }) + return { code: 0, out: teamViewTable(formatted) + '\n', err: '' } +} + +export const teamsCommand: CliCommandDefinition = { + name: 'teams', + order: 40, + description: 'Enterprise teams (list, view).', + usage: 'teams list|view [args] [--pattern P] [--json] [--verbose] [--help|-h]', + subcommands: [...SUBCOMMANDS], + flagOptions: ['--pattern', '--json', '--verbose'], + help: { + title: 'teams — list and inspect enterprise teams', + synopsis: ` teams list [--pattern P] [--json] + teams view NAME|UID [--json] [--verbose]`, + description: + ' Requires an enterprise account. list loads teams; view shows one team with roles and users.', + arguments: ` list Table of teams (default columns: restricts, node, user/role counts). + view Details for one team by name or team_uid.`, + options: ` --pattern list only: filter by name/uid substring. + --json Emit JSON instead of a table. + --verbose view only: include numeric node/user/role ids. + --help, -h Show this help.`, + examples: ` teams list + teams list --pattern eng + teams view "Engineering" --verbose`, + seeAlso: ' users list, sync, login', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(teamsCommand), err: '' } + } + const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub + if (!SUBCOMMANDS.includes(sub)) { + return { + code: 1, + out: '', + err: `teams: unknown subcommand "${parsed.positional[0]}". Try: teams --help\n`, + } + } + try { + switch (sub) { + case 'list': + return await runList(host, parsed) + case 'view': + return await runView(host, parsed) + } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`teams ${sub}`, e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/users.ts b/KeeperSdk/src/cli/commands/users.ts new file mode 100644 index 00000000..ccc26746 --- /dev/null +++ b/KeeperSdk/src/cli/commands/users.ts @@ -0,0 +1,115 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { getOpt, hasOpt, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { + formatUsersTable, + renderUsersAsciiTable, + SUPPORTED_USER_COLUMNS, +} from '../../users/listUsers' +import type { UserColumnInput } from '../../users/userTypes' +import { formatUserView, userViewTable } from '../../users/viewUser' + +const SUBCOMMANDS = ['list', 'view'] as const +type Sub = (typeof SUBCOMMANDS)[number] + +function parseColumns(raw: string | undefined): UserColumnInput[] | '*' | undefined { + const trimmed = raw?.trim() + if (!trimmed) return undefined + if (trimmed === '*') return '*' + return trimmed + .split(',') + .map((p) => p.trim()) + .filter((p) => p.length > 0) as UserColumnInput[] +} + +async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listUsers', 'users list') + if (cap) return cap + await v.sync!() + const pattern = getOpt(parsed.opts, 'pattern') ?? null + const columns = parseColumns(getOpt(parsed.opts, 'columns')) + const rows = await v.listUsers!({ pattern, columns }) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } + } + if (rows.length === 0) { + return { + code: 0, + out: pattern ? `(no users matched "${pattern}")\n` : '(no users)\n', + err: '', + } + } + const table = formatUsersTable(rows, { columns }) + return { code: 0, out: renderUsersAsciiTable(table) + '\n', err: '' } +} + +async function runView(host: KeeperCliHost, parsed: ParsedCli): Promise { + const id = parsed.positional[1] + if (!id) { + return { code: 1, out: '', err: 'users view: missing email or user id. Usage: users view \n' } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'viewUser', 'users view') + if (cap) return cap + const view = await v.viewUser!(id) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(view, null, 2) + '\n', err: '' } + } + const formatted = formatUserView(view, { verbose: hasOpt(parsed.opts, 'verbose') }) + return { code: 0, out: userViewTable(formatted) + '\n', err: '' } +} + +export const usersCommand: CliCommandDefinition = { + name: 'users', + order: 41, + description: 'Enterprise users (list, view).', + usage: 'users list|view [args] [--pattern P] [--columns C] [--json] [--verbose] [--help|-h]', + subcommands: [...SUBCOMMANDS], + flagOptions: ['--pattern', '--columns', '--json', '--verbose'], + help: { + title: 'users — list and inspect enterprise users', + synopsis: ` users list [--pattern P] [--columns cols] + users view EMAIL|ID [--json] [--verbose]`, + description: ' Requires an enterprise account.', + arguments: ` list Table of enterprise users. + view Details for one user by email or enterprise_user_id.`, + options: ` --pattern list only: filter by name/email substring. + --columns list only: comma-separated columns or * (supported: ${SUPPORTED_USER_COLUMNS.join(', ')}). + --json Emit JSON instead of a table. + --verbose view only: include UIDs in team/role rows. + --help, -h Show this help.`, + examples: ` users list + users list --pattern @acme.com --columns name,status,node + users view user@example.com`, + seeAlso: ' teams list, sync, login', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(usersCommand), err: '' } + } + const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub + if (!SUBCOMMANDS.includes(sub)) { + return { + code: 1, + out: '', + err: `users: unknown subcommand "${parsed.positional[0]}". Try: users --help\n`, + } + } + try { + switch (sub) { + case 'list': + return await runList(host, parsed) + case 'view': + return await runView(host, parsed) + } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`users ${sub}`, e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/vault.ts b/KeeperSdk/src/cli/commands/vault.ts new file mode 100644 index 00000000..9e374bbf --- /dev/null +++ b/KeeperSdk/src/cli/commands/vault.ts @@ -0,0 +1,56 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { hasOpt, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureCapability, ensureSession } from '../commandHelpers' + +async function runSummary(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'getSummary', 'vault summary') + if (cap) return cap + await v.sync!() + const summary = v.getSummary!() + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(summary, null, 2) + '\n', err: '' } + } + const lines = [ + `records: ${summary.recordCount}`, + `shared_folders: ${summary.sharedFolderCount}`, + `teams: ${summary.teamCount}`, + `folders: ${summary.folderCount}`, + ] + return { code: 0, out: lines.join('\n') + '\n', err: '' } +} + +export const vaultCommand: CliCommandDefinition = { + name: 'vault', + order: 25, + description: 'Vault summary counts (records, folders, teams, …).', + usage: 'vault summary [--json] [--help|-h]', + subcommands: ['summary'], + flagOptions: ['--json'], + help: { + title: 'vault — vault-wide statistics', + synopsis: ' vault summary [--json]', + description: ' Runs sync, then prints counts from the local vault cache.', + arguments: ' summary Print record, shared folder, team, and user-folder counts.', + options: ' --json Emit JSON.\n --help, -h Show this help.', + examples: ' vault summary\n vault summary --json', + seeAlso: ' sync, records list, folders tree', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(vaultCommand), err: '' } + } + const sub = parsed.positional[0]?.toLowerCase() ?? 'summary' + if (sub !== 'summary') { + return { code: 1, out: '', err: 'Usage: vault summary\n' } + } + try { + return await runSummary(host, parsed) + } catch (e) { + return { code: 1, out: '', err: host.formatError('vault summary', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts index ee696090..bf8c89f5 100644 --- a/KeeperSdk/src/cli/index.ts +++ b/KeeperSdk/src/cli/index.ts @@ -6,7 +6,11 @@ import { logoutCommand } from './commands/logout' import { recordsCommand } from './commands/records' import { registerDeviceCommand } from './commands/registerDevice' import { restoreSessionCommand } from './commands/restoreSession' +import { sharedFoldersCommand } from './commands/sharedFolders' import { syncCommand } from './commands/sync' +import { teamsCommand } from './commands/teams' +import { usersCommand } from './commands/users' +import { vaultCommand } from './commands/vault' let registryInitialized = false @@ -19,8 +23,12 @@ export function ensureKeeperCliRegistry(): void { registerCliCommand(registerDeviceCommand) registerCliCommand(restoreSessionCommand) registerCliCommand(syncCommand) + registerCliCommand(vaultCommand) registerCliCommand(recordsCommand) registerCliCommand(foldersCommand) + registerCliCommand(sharedFoldersCommand) + registerCliCommand(teamsCommand) + registerCliCommand(usersCommand) registerCliCommand(logoutCommand) } @@ -85,6 +93,10 @@ export { export { runLogoutCommand, logoutCommand } from './commands/logout' export { recordsCommand } from './commands/records' export { foldersCommand } from './commands/folders' +export { sharedFoldersCommand } from './commands/sharedFolders' +export { teamsCommand } from './commands/teams' +export { usersCommand } from './commands/users' +export { vaultCommand } from './commands/vault' export { registerDeviceCommand } from './commands/registerDevice' export { helpCommand } from './commands/help' export { restoreSessionCommand } from './commands/restoreSession' diff --git a/KeeperSdk/src/cli/parser.ts b/KeeperSdk/src/cli/parser.ts index 82e0f584..6d341c95 100644 --- a/KeeperSdk/src/cli/parser.ts +++ b/KeeperSdk/src/cli/parser.ts @@ -135,6 +135,12 @@ function loadBuiltinsInto(parser: KeeperCliParser): void { restoreSessionCommand, } = require('./commands/restoreSession') as typeof import('./commands/restoreSession') const { syncCommand } = require('./commands/sync') as typeof import('./commands/sync') + const { vaultCommand } = require('./commands/vault') as typeof import('./commands/vault') + const { + sharedFoldersCommand, + } = require('./commands/sharedFolders') as typeof import('./commands/sharedFolders') + const { teamsCommand } = require('./commands/teams') as typeof import('./commands/teams') + const { usersCommand } = require('./commands/users') as typeof import('./commands/users') parser.addCommands([ helpCommand, @@ -142,8 +148,12 @@ function loadBuiltinsInto(parser: KeeperCliParser): void { registerDeviceCommand, restoreSessionCommand, syncCommand, + vaultCommand, recordsCommand, foldersCommand, + sharedFoldersCommand, + teamsCommand, + usersCommand, logoutCommand, ]) } diff --git a/KeeperSdk/src/cli/table.ts b/KeeperSdk/src/cli/table.ts new file mode 100644 index 00000000..499f206c --- /dev/null +++ b/KeeperSdk/src/cli/table.ts @@ -0,0 +1,8 @@ +/** Fixed-width column formatter. Last column is left unpadded. */ +export function formatTable(headers: string[], rows: string[][]): string { + if (rows.length === 0) return '' + const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))) + const fmt = (cells: string[]): string => + cells.map((s, i) => (i === cells.length - 1 ? s : (s ?? '').padEnd(widths[i]))).join(' ') + return [fmt(headers), ...rows.map(fmt)].join('\n') + '\n' +} diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts index 343d2945..67d3e990 100644 --- a/KeeperSdk/src/cli/types.ts +++ b/KeeperSdk/src/cli/types.ts @@ -5,6 +5,15 @@ import type { FolderTreeBuildOptions } from '../folders/folderTree' import type { GetFolderOptions, GetFolderResult } from '../folders/getFolder' import type { ListFolderOptions, ListFolderResult } from '../folders/listFolder' import type { MkdirOptions } from '../folders/addFolder' +import type { RenameFolderResult } from '../folders/updateFolder' +import type { DeleteFolderResult } from '../folders/deleteFolder' +import type { ListSharedFolderRow, ListSharedFoldersOptions } from '../sharedFolders/listSharedFolders' +import type { ListTeamRow, ListTeamsOptions } from '../teams/listTeams' +import type { TeamView } from '../teams/viewTeam' +import type { ListUserRow, ListUsersOptions } from '../users/userTypes' +import type { UserView } from '../users/userTypes' +import type { RecordShareInfo } from '../sharing/Sharing' +import type { VaultSummary } from '../vault/KeeperVault' export type CliResult = { code: number @@ -20,7 +29,10 @@ export type ParsedCli = { opts: Map } -/** Vault surface used by CLI command handlers. Folder methods are optional — commands check at runtime. */ +/** + * Vault surface for CLI handlers. Methods beyond session/sync/records are optional; + * commands call `ensureCapability` so thin hosts fail with a clear message. + */ export type KeeperCliVault = { readonly isLoggedIn: boolean login(username: string, password: string): Promise @@ -31,6 +43,11 @@ export type KeeperCliVault = { getSharedFolders(): DSharedFolder[] registerDevice(deviceToken: string, privateKey: string, options?: { username?: string }): Promise restoreSession(input: SessionRestoreInput): Promise + getSummary?: () => VaultSummary + findRecord?: (uidOrTitle: string) => DRecord | undefined + findRecords?: (criteria: string) => DRecord[] + getRecordShareInfo?: (recordUid: string) => Promise + listSharedFolders?: (options?: ListSharedFoldersOptions) => ListSharedFolderRow[] listFolder?: (options?: ListFolderOptions) => Promise tree?: (options?: FolderTreeBuildOptions) => Promise changeDirectory?: (path: string) => Promise @@ -38,6 +55,12 @@ export type KeeperCliVault = { getWorkingFolderDisplayName?: () => string getFolder?: (uidOrName: string, options?: GetFolderOptions) => Promise mkdir?: (path: string, options?: MkdirOptions) => Promise<{ folderUid: string; success: boolean; message?: string }> + renameFolder?: (folderPath: string, newName: string) => Promise + rmdir?: (patterns: string[], options?: { force?: boolean }) => Promise + listTeams?: (options?: ListTeamsOptions) => Promise + viewTeam?: (identifier: string) => Promise + listUsers?: (options?: ListUsersOptions) => Promise + viewUser?: (identifier: string) => Promise } /** Host adapter (browser shell, Node script, tests). `readTextFile` is optional. */ diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 55259f19..a8d52b00 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -104,7 +104,15 @@ import type { TeamUserResult, FormattedTeamUserTable, } from '../users/userTypes' -import { ConsoleLogger, LogLevel, KeeperSdkError, extractErrorMessage, SdkDefaults, ResultCodes } from '../utils' +import { + ConsoleLogger, + LogLevel, + KeeperSdkError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + ResultCodes, +} from '../utils' import type { ILogger } from '../utils' enum VaultStatus { diff --git a/examples/sdk_example/src/utils/shellCliHost.ts b/examples/sdk_example/src/utils/shellCliHost.ts index 9858296c..e4fceb5d 100644 --- a/examples/sdk_example/src/utils/shellCliHost.ts +++ b/examples/sdk_example/src/utils/shellCliHost.ts @@ -70,6 +70,11 @@ function asCliVault(v: VaultInstance): KeeperCliVault { getSharedFolders: () => v.getSharedFolders(), registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), restoreSession: (input) => v.restoreSession(input), + getSummary: () => v.getSummary(), + findRecord: (uidOrTitle) => v.findRecord(uidOrTitle), + findRecords: (criteria) => v.findRecords(criteria), + getRecordShareInfo: (uid) => v.getRecordShareInfo(uid), + listSharedFolders: (options) => v.listSharedFolders(options), listFolder: (options) => v.listFolder(options), tree: (options) => v.tree(options), changeDirectory: (path) => v.changeDirectory(path), @@ -77,6 +82,12 @@ function asCliVault(v: VaultInstance): KeeperCliVault { getWorkingFolderDisplayName: () => v.getWorkingFolderDisplayName(), getFolder: (uidOrName, options) => v.getFolder(uidOrName, options), mkdir: (path, options) => v.mkdir(path, options), + renameFolder: (path, name) => v.renameFolder(path, name), + rmdir: (patterns, options) => v.rmdir(patterns, options), + listTeams: (options) => v.listTeams(options), + viewTeam: (id) => v.viewTeam(id), + listUsers: (options) => v.listUsers(options), + viewUser: (id) => v.viewUser(id), } } diff --git a/shellcomponent/README.md b/shellcomponent/README.md index b78b83fe..08fd0d4e 100644 --- a/shellcomponent/README.md +++ b/shellcomponent/README.md @@ -77,9 +77,31 @@ If TypeScript complains about unknown intrinsic elements, extend **`JSX.Intrinsi Boolean attributes follow HTML rules: include the attribute name to enable, omit to disable. +## CLI commands (in-browser) + +The shell dispatches the Keeper SDK CLI (`dispatchCliLine`). Built-in commands include: + +| Command | Purpose | +|---------|---------| +| `help` | List commands or show help for one | +| `login` / `logout` | Authenticate (password via masked prompt; never on the CLI line) | +| `register-device` | Store device token + key for session-token login | +| `restore-session` | Resume from extension session JSON (`--from-json`) | +| `sync` | Vault sync | +| `vault summary` | Record / folder / team counts | +| `records` | `list`, `get`, `find`, `share-info` | +| `folders` | `list`, `tree`, `ls`, `pwd`, `cd`, `mkdir`, `rename`, `rmdir`, `get` | +| `shared-folders` | Shared folders with optional `--verbose` counts | +| `teams` | Enterprise `list`, `view` | +| `users` | Enterprise `list`, `view` | + +Use ` --help` for full docs. Tab completes command names, subcommands, and flags. + +Write/mutate operations (add/update/delete users or teams, share record, etc.) are available on `KeeperVault` in code; CLI coverage focuses on list/get/view flows usable from the shell. + ## Programmatic API (optional) -The package also exports helpers used by the shell (CLI dispatch, completion, vault helpers). See **`keeper-shell.d.ts`** and **`src/index.ts`** for **`dispatchCliLine`**, **`completeCliLine`**, **`setShellCliContext`**, **`resetShellVault`**, **`loginWithCredentials`**, and the **`KeeperShell`** / **`WebConsoleElement`** classes. +The package also exports helpers used by the shell (CLI dispatch, completion, vault helpers). See **`keeper-shell.d.ts`** and **`src/index.ts`** for **`dispatchCliLine`**, **`completeCliLine`**, **`setShellCliContext`**, **`resetShellVault`**, **`loginWithCredentials`**, **`loginWithSessionToken`**, and the **`KeeperShell`** / **`WebConsoleElement`** classes. ## Layout note (non-embed) diff --git a/shellcomponent/keeper-shell.d.ts b/shellcomponent/keeper-shell.d.ts index a65c7c80..f77bf52a 100644 --- a/shellcomponent/keeper-shell.d.ts +++ b/shellcomponent/keeper-shell.d.ts @@ -47,6 +47,11 @@ export function completeCliLine(line: string): { export function setShellCliContext(next: ShellCliContext): void; export function resetShellVault(): void; export function loginWithCredentials(username: string, password: string): Promise; +export function loginWithSessionToken( + username: string, + sessionToken: string, + options?: { plainToken?: boolean } +): Promise; declare global { interface HTMLElementTagNameMap { diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts index e663a943..ea26bec6 100644 --- a/shellcomponent/src/KeeperShell.ts +++ b/shellcomponent/src/KeeperShell.ts @@ -1086,8 +1086,11 @@ export class KeeperShell extends HTMLElement { const remote = this._remoteApiPrefix(); if (remote === null) { term.writeln("\x1b[1mWeb console\x1b[0m — commands run in this browser (Keeper SDK + CLI)."); + term.writeln( + "Type `help` — records, folders, shared-folders, teams, users, vault summary (see each COMMAND --help)." + ); term.writeln("Tab completion and masked password entry are handled locally."); - term.writeln("Optional: `keeper-host` attribute for vault region."); + term.writeln("Optional: `keeper-host` attribute (or VITE_KEEPER_HOST) for vault region."); } else { term.writeln("\x1b[1mWeb console\x1b[0m — commands execute on your backend API."); term.writeln( diff --git a/shellcomponent/src/cli/cliComplete.ts b/shellcomponent/src/cli/cliComplete.ts index c2f2fece..77bed47e 100644 --- a/shellcomponent/src/cli/cliComplete.ts +++ b/shellcomponent/src/cli/cliComplete.ts @@ -1,5 +1,5 @@ /** - * Tab-completion metadata for the keeper-shell CLI (from SDK command registry). + * Tab-completion for keeper-shell (SDK command registry). */ import { getCliCommand, listCliCommandNames } from "@keeper-security/keeper-sdk-javascript"; @@ -47,16 +47,19 @@ export function completeCliLine(line: string): CliCompleteResult { } const cmd = lc(words[0]); + const def = getCliCommand(cmd); if (words.length === 1) { if (stub.startsWith("-")) { const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); return { base: baseFor(stub.length), candidates: [...hits] }; } - const subs = getCliCommand(cmd)?.subcommands; + const subs = def?.subcommands; if (subs?.length) { const hits = subs.filter((s) => lc(s).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: [...hits] }; + if (hits.length > 0) { + return { base: baseFor(stub.length), candidates: [...hits] }; + } } if (completesNewWord || stub.length > 0) { const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); @@ -64,5 +67,21 @@ export function completeCliLine(line: string): CliCompleteResult { } } + if (words.length >= 2 && def?.subcommands) { + const subLc = lc(words[1]); + const knownSub = def.subcommands.some((s) => lc(s) === subLc); + if (knownSub && (stub.startsWith("-") || completesNewWord)) { + const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); + return { base: baseFor(stub.length), candidates: [...hits] }; + } + } + + if (words.length >= 2 && !def?.subcommands && (stub.startsWith("-") || completesNewWord)) { + const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); + if (hits.length > 0) { + return { base: baseFor(stub.length), candidates: [...hits] }; + } + } + return { base: line, candidates: [] }; } diff --git a/shellcomponent/src/cli/cliContext.ts b/shellcomponent/src/cli/cliContext.ts index 5ef186b3..caeec3db 100644 --- a/shellcomponent/src/cli/cliContext.ts +++ b/shellcomponent/src/cli/cliContext.ts @@ -19,10 +19,33 @@ export function getShellKeeperHost(): string | undefined { return undefined; } +const VITE_ENV_MAP: Record = { + KEEPER_USERNAME: "VITE_KEEPER_USERNAME", + KEEPER_PASSWORD: "VITE_KEEPER_PASSWORD", + KEEPER_SESSION_TOKEN: "VITE_KEEPER_SESSION_TOKEN", + KEEPER_HOST: "VITE_KEEPER_HOST", + REGISTER_DEVICE_TOKEN: "VITE_REGISTER_DEVICE_TOKEN", + REGISTER_DEVICE_PRIVATE_KEY: "VITE_REGISTER_DEVICE_PRIVATE_KEY", + RESTORE_SESSION_JSON: "VITE_RESTORE_SESSION_JSON", +}; + +function readViteEnv(name: string): string | undefined { + try { + const viteKey = VITE_ENV_MAP[name]; + if (!viteKey || typeof import.meta === "undefined" || !import.meta.env) return undefined; + const v = import.meta.env[viteKey as keyof ImportMetaEnv] as string | undefined; + const t = typeof v === "string" ? v.trim() : ""; + return t.length > 0 ? t : undefined; + } catch { + return undefined; + } +} + +/** Node `process.env` in SSR/build; `import.meta.env.VITE_*` in the browser dev bundle. */ export function envString(name: string): string | undefined { if (typeof process !== "undefined" && process.env) { const v = process.env[name]; - return typeof v === "string" && v.length > 0 ? v : undefined; + if (typeof v === "string" && v.length > 0) return v; } - return undefined; + return readViteEnv(name); } diff --git a/shellcomponent/src/cli/cliDispatch.ts b/shellcomponent/src/cli/cliDispatch.ts index 3dfa954a..3976c181 100644 --- a/shellcomponent/src/cli/cliDispatch.ts +++ b/shellcomponent/src/cli/cliDispatch.ts @@ -1,8 +1,12 @@ -import "./mkdirCommand.js"; -import { dispatchCliLine as sdkDispatchCliLine } from "@keeper-security/keeper-sdk-javascript"; +import { + dispatchCliLine as sdkDispatchCliLine, + ensureKeeperCliRegistry, +} from "@keeper-security/keeper-sdk-javascript"; import { shellKeeperCliHost } from "./keeperCliHost.js"; import type { CliResult } from "./types.js"; +ensureKeeperCliRegistry(); + const rawMax = typeof process !== "undefined" && process.env?.CLI_MAX_LINE_LENGTH ? Number(process.env.CLI_MAX_LINE_LENGTH) diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts index 1c857162..9006f46b 100644 --- a/shellcomponent/src/cli/keeperCliHost.ts +++ b/shellcomponent/src/cli/keeperCliHost.ts @@ -64,13 +64,9 @@ function formatKeeperClientError(context: string, e: unknown): string { : `\n Region/host: default production (US). EU/AU/CA/JP tenants often need keeper-host on or KEEPER_HOST.`; return ( `${context}: ${base}\n` + - ` Why only this text: browsers intentionally hide the underlying HTTP status / CORS detail for many failed requests.\n` + - ` Typical causes (your password may still be correct):\n` + - ` • CORS — Keeper’s API may not allow calls from this page’s origin (common for http://localhost in dev).\n` + - ` • Network — offline, DNS, VPN, corporate proxy, or firewall blocking HTTPS to Keeper.\n` + - ` • Mixed content — page is http while the API is https; load the dev page over https.\n` + + ` Browser could not reach Keeper (CORS, network, or wrong region).` + hostHint + - `\n Mitigation: run login from a backend your page trusts (set web-console remote + api-base), or use the SDK from Node.js.\n` + `\n Dev: run shellcomponent with \`npm run dev\` (same-origin proxy). Prod: set keeper-host or use remote + api-base.\n` ); } @@ -87,6 +83,11 @@ function asCliVault(v: VaultInstance): KeeperCliVault { getSharedFolders: () => v.getSharedFolders(), registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), restoreSession: (input) => v.restoreSession(input), + getSummary: () => v.getSummary(), + findRecord: (uidOrTitle) => v.findRecord(uidOrTitle), + findRecords: (criteria) => v.findRecords(criteria), + getRecordShareInfo: (uid) => v.getRecordShareInfo(uid), + listSharedFolders: (options) => v.listSharedFolders(options), listFolder: (options) => v.listFolder(options), tree: (options) => v.tree(options), changeDirectory: (path) => v.changeDirectory(path), @@ -94,6 +95,12 @@ function asCliVault(v: VaultInstance): KeeperCliVault { getWorkingFolderDisplayName: () => v.getWorkingFolderDisplayName(), getFolder: (uidOrName, options) => v.getFolder(uidOrName, options), mkdir: (path, options) => v.mkdir(path, options), + renameFolder: (path, name) => v.renameFolder(path, name), + rmdir: (patterns, options) => v.rmdir(patterns, options), + listTeams: (options) => v.listTeams(options), + viewTeam: (id) => v.viewTeam(id), + listUsers: (options) => v.listUsers(options), + viewUser: (id) => v.viewUser(id), }; } diff --git a/shellcomponent/src/cli/keeperCommands.ts b/shellcomponent/src/cli/keeperCommands.ts index 419c9141..dcabc819 100644 --- a/shellcomponent/src/cli/keeperCommands.ts +++ b/shellcomponent/src/cli/keeperCommands.ts @@ -14,10 +14,13 @@ export async function loginWithCredentials(username: string, password: string): return sdkLoginWithCredentials(shellKeeperCliHost, username, password); } -export async function loginWithSessionTokenCredentials( +export async function loginWithSessionToken( username: string, sessionToken: string, options?: { plainToken?: boolean } ): Promise { return sdkLoginWithSessionToken(shellKeeperCliHost, username, sessionToken, options); } + +/** @deprecated Use {@link loginWithSessionToken}. */ +export const loginWithSessionTokenCredentials = loginWithSessionToken; diff --git a/shellcomponent/src/cli/mkdirCommand.ts b/shellcomponent/src/cli/mkdirCommand.ts deleted file mode 100644 index f0420ee7..00000000 --- a/shellcomponent/src/cli/mkdirCommand.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - registerCliCommand, - rejectUnknownOptions, - type CliCommandDefinition, -} from "@keeper-security/keeper-sdk-javascript"; - -const MKDIR_ALLOWED = new Set(["p", "parents"]); - -export const mkdirShellCommand: CliCommandDefinition = { - name: "mkdir", - order: 900, - description: - "Host mkdir is disabled in keeper-shell (no filesystem). Use `api-base` for an HTTP CLI backend, or SDK folder APIs.", - usage: "mkdir [-p|--parents] [--] ; mkdir --help", - flagOptions: ["-p", "--parents"], - allowedOptions: MKDIR_ALLOWED, - help: { - title: "mkdir — host filesystem directory (disabled in embedded shell)", - synopsis: " mkdir [-p|--parents] [--] RELATIVE_PATH", - description: ` In keeper-shell with in-browser SDK transport, host mkdir is not available (no - sandboxed filesystem). Set api-base to an HTTP CLI backend if you need - this command, or use Keeper vault folder APIs in code.`, - options: ` -p, --parents (remote server only.) - -- End of options.`, - note: " Vault folder operations live on KeeperVault (mkdir, addFolder, …) in the SDK.", - }, - async run(_host, parsed) { - const bad = rejectUnknownOptions(parsed, MKDIR_ALLOWED, "mkdir"); - if (bad) return bad; - return { - code: 1, - out: "", - err: - "mkdir: not available in keeper-shell (no host filesystem). " + - "Set api-base to an HTTP CLI backend for workspace mkdir, or use Keeper vault folder APIs.\n", - }; - }, -}; - -registerCliCommand(mkdirShellCommand); diff --git a/shellcomponent/src/index.ts b/shellcomponent/src/index.ts index e1c1c0df..f864268f 100644 --- a/shellcomponent/src/index.ts +++ b/shellcomponent/src/index.ts @@ -14,4 +14,8 @@ export { completeCliLine } from "./cli/cliComplete.js"; export type { CliResult } from "./cli/types.js"; export type { ShellCliContext } from "./cli/cliContext.js"; export { setShellCliContext } from "./cli/cliContext.js"; -export { resetShellVault, loginWithCredentials } from "./cli/keeperCommands.js"; +export { + resetShellVault, + loginWithCredentials, + loginWithSessionToken, +} from "./cli/keeperCommands.js"; diff --git a/shellcomponent/src/vite-env.d.ts b/shellcomponent/src/vite-env.d.ts index 45ced1bb..8305b480 100644 --- a/shellcomponent/src/vite-env.d.ts +++ b/shellcomponent/src/vite-env.d.ts @@ -1,10 +1,14 @@ /// interface ImportMetaEnv { - /** Must match the username you use with `login` (device lookup is keyed by last_login/user). */ readonly VITE_KEEPER_DEV_DEVICE_USER?: string; - /** Base64url device token from `.keeper/config.json` (same as `device_token`). */ readonly VITE_KEEPER_DEV_DEVICE_TOKEN?: string; - /** Base64url EC private key from `.keeper/config.json` (same as `private_key`). */ readonly VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY?: string; + readonly VITE_KEEPER_USERNAME?: string; + readonly VITE_KEEPER_PASSWORD?: string; + readonly VITE_KEEPER_SESSION_TOKEN?: string; + readonly VITE_KEEPER_HOST?: string; + readonly VITE_REGISTER_DEVICE_TOKEN?: string; + readonly VITE_REGISTER_DEVICE_PRIVATE_KEY?: string; + readonly VITE_RESTORE_SESSION_JSON?: string; } From 61c09d4fd263e8499166e198abbc23bcadb77ecd Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 1 Jun 2026 11:06:23 +0530 Subject: [PATCH 10/23] added a fix for hmc fail on browser platform --- KeeperSdk/src/platform/browser/platform.ts | 34 +++++++++++++--------- shellcomponent/.env.example | 12 ++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 shellcomponent/.env.example diff --git a/KeeperSdk/src/platform/browser/platform.ts b/KeeperSdk/src/platform/browser/platform.ts index 5a0e639e..95938970 100644 --- a/KeeperSdk/src/platform/browser/platform.ts +++ b/KeeperSdk/src/platform/browser/platform.ts @@ -5,20 +5,23 @@ import type { ConfigLoader } from '../../auth/config' import * as asmCrypto from 'asmcrypto.js' import type { SdkPlatform, SdkReadline } from '../types' -type AsmCryptoModule = typeof asmCrypto & { - HMAC: { sign: (data: Uint8Array, key: Uint8Array, hash: unknown) => Uint8Array } - SHA1: unknown - SHA256: unknown - SHA512: unknown +type HmacCtor = new (key: Uint8Array) => { + process(data: Uint8Array): void + finish(): void + result: Uint8Array } -const asm = asmCrypto as AsmCryptoModule +const asm = asmCrypto as typeof asmCrypto & { + HmacSha1: HmacCtor + HmacSha256: HmacCtor + HmacSha512: HmacCtor +} -const HMAC_HASH = { - sha1: asm.SHA1, - sha256: asm.SHA256, - sha512: asm.SHA512, -} as const +const HMAC_IMPL: Record<'sha1' | 'sha256' | 'sha512', HmacCtor> = { + sha1: asm.HmacSha1, + sha256: asm.HmacSha256, + sha512: asm.HmacSha512, +} const BROWSER_READLINE_MSG = 'Interactive readline is not available in the browser. Use keeper-shell password transport or a custom authUI.' @@ -47,11 +50,14 @@ export const browserSdkPlatform: SdkPlatform = { }, hmac(algorithm, key, data) { - const hash = HMAC_HASH[algorithm] - if (!hash) { + const Ctor = HMAC_IMPL[algorithm] + if (!Ctor) { throw new KeeperSdkError(`Unsupported HMAC algorithm: ${algorithm}`, ResultCodes.UNSUPPORTED_2FA_CHANNEL) } - return asm.HMAC.sign(data, key, hash) + const h = new Ctor(key) + h.process(data) + h.finish() + return h.result }, createFileConfigLoader(): ConfigLoader { diff --git a/shellcomponent/.env.example b/shellcomponent/.env.example new file mode 100644 index 00000000..acf00699 --- /dev/null +++ b/shellcomponent/.env.example @@ -0,0 +1,12 @@ +# Optional dev-only (Vite). Copy to .env.local — never commit secrets. +# Device pre-seed (same shape as ~/.keeper/config.json); username must match `login`. +# VITE_KEEPER_DEV_DEVICE_USER=you@example.com +# VITE_KEEPER_DEV_DEVICE_TOKEN= +# VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY= + +# VITE_KEEPER_HOST=keepersecurity.eu +# VITE_KEEPER_USERNAME= +# VITE_KEEPER_SESSION_TOKEN= +# VITE_REGISTER_DEVICE_TOKEN= +# VITE_REGISTER_DEVICE_PRIVATE_KEY= +# VITE_RESTORE_SESSION_JSON= From 3bea9354b3dfc733e72499728c863ba33bfcb1ea Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 1 Jun 2026 15:18:51 +0530 Subject: [PATCH 11/23] added changes to countertobuffer logic --- KeeperSdk/src/records/Totp.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/KeeperSdk/src/records/Totp.ts b/KeeperSdk/src/records/Totp.ts index a75a2fa7..92980757 100644 --- a/KeeperSdk/src/records/Totp.ts +++ b/KeeperSdk/src/records/Totp.ts @@ -73,10 +73,11 @@ export function parseTotpUrl(url: string): TotpParams | null { } } -function counterToBuffer(counter: number): Buffer { - const buf = Buffer.alloc(8) - buf.writeUInt32BE(Math.floor(counter / UINT32_MAX), 0) - buf.writeUInt32BE(counter % UINT32_MAX, 4) +function counterToBuffer(counter: number): Uint8Array { + const buf = new Uint8Array(8) + const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength) + view.setUint32(0, Math.floor(counter / UINT32_MAX), false) + view.setUint32(4, counter >>> 0, false) return buf } From d0080d03eb24098ed701d436030d03b748b46d29 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 1 Jun 2026 18:26:03 +0530 Subject: [PATCH 12/23] altered state for refresh rate issue --- KeeperSdk/src/storage/InMemoryStorage.ts | 3 + shellcomponent/README.md | 24 +- shellcomponent/keeper-shell.d.ts | 13 +- shellcomponent/src/KeeperShell.ts | 302 ++++++----------------- shellcomponent/src/cli/keeperCliHost.ts | 45 +++- shellcomponent/src/dev-bootstrap.ts | 7 +- shellcomponent/vite.config.ts | 109 ++++---- 7 files changed, 186 insertions(+), 317 deletions(-) diff --git a/KeeperSdk/src/storage/InMemoryStorage.ts b/KeeperSdk/src/storage/InMemoryStorage.ts index 89424b97..1ddbe973 100644 --- a/KeeperSdk/src/storage/InMemoryStorage.ts +++ b/KeeperSdk/src/storage/InMemoryStorage.ts @@ -37,6 +37,9 @@ export class InMemoryStorage implements VaultStorage { public async put(item: VaultStorageData): Promise { const kind = item.kind + if (!kind) { + throw new Error('VaultStorageData missing kind') + } if (!this.store.has(kind)) { this.store.set(kind, new Map()) } diff --git a/shellcomponent/README.md b/shellcomponent/README.md index 08fd0d4e..ed6a0702 100644 --- a/shellcomponent/README.md +++ b/shellcomponent/README.md @@ -1,6 +1,6 @@ # Keeper shell component (`@keeper-security/keeper-shell-component`) -Web component that embeds the Keeper CLI in the browser: **``** and **``** (same behavior, two tag names). The default mode runs the in-browser Keeper JavaScript SDK and xterm.js; you can optionally point CLI traffic at your own HTTP relay. +Web component that embeds the Keeper CLI in the browser: **``** and **``** (same behavior, two tag names). Commands run in-page via the Keeper JavaScript SDK and xterm.js. ## Requirements @@ -70,9 +70,7 @@ If TypeScript complains about unknown intrinsic elements, extend **`JSX.Intrinsi | **`height`** | Height of the terminal region (e.g. `360px`, `24rem`). Default **`320px`**. | | **`collapsed`** | If present, the terminal panel starts hidden; only the shell toggle control is shown until the user opens it. | | **`embed`** | If present, renders a full in-page terminal **without** the open/hide control. | -| **`remote`** | If present, the CLI uses HTTP (`POST` to **`${apiBase}/cli`**, etc.) instead of the in-browser SDK. | -| **`api-base`** | Base URL for the remote CLI (no trailing slash). Default **`/api`** when **`remote`** is set. | -| **`keeper-host`** | Optional Keeper vault / region host override when using the in-browser SDK. | +| **`keeper-host`** | Optional Keeper vault / region host override (e.g. `keepersecurity.eu`). | | **`mask-input`** | If present, new prompts start with masked input (`*`); **Ctrl+O** toggles masking in the shell. | Boolean attributes follow HTML rules: include the attribute name to enable, omit to disable. @@ -122,7 +120,7 @@ npm run dev Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell. -The dev page runs the **Keeper SDK in the browser** (no `remote` / `api-base`). To avoid CORS, dev wires a **same-origin proxy** — see [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md). Only `npm run dev` is needed. +The dev page runs the **Keeper SDK in the browser**. To avoid CORS locally, dev wires a **same-origin proxy** — see [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md). Only `npm run dev` is needed. ```text restore-session --from-json /dev/keeper-session.json --sync @@ -135,12 +133,22 @@ Set region on the element when needed: `` or `` (JSX: use the string tag name `"web-console"`). +4. **Region** — set `keeper-host` when not on US prod (e.g. `keepersecurity.eu`). +5. **Networking** — the SDK calls Keeper REST/WSS from the browser. Your origin must allow it (CORS) or proxy Keeper through your app origin (same pattern as [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md), implemented in your gateway/nginx, not Vite). +6. **`restore-session --from-json`** — in production use inline JSON or an `https://` URL on your origin; local file paths work only in Vite dev (`/@fs`, `/dev/keeper-session.json`). + +The bundle is self-contained (no separate CLI server). Do not expect `remote` / `api-base` attributes — those were removed. ## Security and networking -- **In-browser SDK** mode performs Keeper API calls from the user’s browser; your page’s origin, CSP, and CORS must allow what the SDK needs. -- **`remote`** mode sends CLI lines to **`api-base`**; only point it at backends you trust and that enforce auth appropriately. +Keeper API calls run from the user’s browser. Your page’s origin, CSP, and CORS (or a reverse proxy on your origin) must allow what the SDK needs. See [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md) for the local dev proxy pattern. ## License diff --git a/shellcomponent/keeper-shell.d.ts b/shellcomponent/keeper-shell.d.ts index f77bf52a..4afab90f 100644 --- a/shellcomponent/keeper-shell.d.ts +++ b/shellcomponent/keeper-shell.d.ts @@ -14,22 +14,11 @@ export const KEEPER_SHELL_TAG: "keeper-shell"; export const WEB_CONSOLE_TAG: "web-console"; export class KeeperShell extends HTMLElement { - /** Base URL when {@link remote} is true (default `/api`). */ - apiBase: string; keeperHost: string; collapsed: boolean; maskInput: boolean; - /** - * When true, CLI uses HTTP (`POST ${apiBase}/cli`, …). - * When false (default), runs in-browser (Keeper SDK). - */ - remote: boolean; - /** - * @deprecated Same as `!remote`. Setting `local` removes `remote`; clearing `local` sets `remote`. - */ - local: boolean; - /** Full-page terminal; no Open/Hide console button. */ embed: boolean; + height: string; } /** `` — extends {@link KeeperShell} (distinct class for dual registration). */ diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts index ea26bec6..08bad5b0 100644 --- a/shellcomponent/src/KeeperShell.ts +++ b/shellcomponent/src/KeeperShell.ts @@ -11,34 +11,22 @@ export const KEEPER_SHELL_TAG = "keeper-shell"; /** Legacy custom element name (same behavior as {@link KEEPER_SHELL_TAG}). */ export const WEB_CONSOLE_TAG = "web-console"; -const ATTR_API_BASE = "api-base"; const ATTR_KEEPER_HOST = "keeper-host"; const ATTR_COLLAPSED = "collapsed"; const ATTR_HEIGHT = "height"; -/** When set, CLI uses HTTP (POST `${apiBase}/cli`, …). Omitted = in-browser Keeper SDK (default for prod FE). */ -const ATTR_REMOTE = "remote"; -/** @deprecated Prefer default in-browser mode; `local` removes the `remote` attribute when set via property. */ -const ATTR_LOCAL = "local"; /** Full in-page terminal only (no show/hide button). */ const ATTR_EMBED = "embed"; /** When set, each new `$ ` prompt starts with masked input (`*` display). Toggle with Ctrl+O. */ const ATTR_MASK_INPUT = "mask-input"; -/** Same as legacy ``: default `/api` when attribute is missing. */ -function normalizeApiBase(raw: string | null): string { - const s = (raw ?? "/api").trim() || "/api"; - return s.replace(/\/$/, "") || "/api"; -} - -/** Resolved at build time; change `../icon.jpg` if the asset moves. */ -const CONSOLE_TOGGLE_ICON_URL = new URL("../icon.jpg", import.meta.url).href; - -/** Same bitmap for both states; `aria-label` / `title` reflect expand vs collapse. */ +/** Same icon for both states; `aria-label` / `title` reflect expand vs collapse. */ function setConsoleToggleButtonUi(btn: HTMLButtonElement, collapsed: boolean): void { const label = collapsed ? "Open console" : "Hide console"; btn.setAttribute("aria-label", label); btn.title = label; - btn.innerHTML = ``; + btn.innerHTML = + ''; } type CliResponse = { @@ -52,36 +40,6 @@ type CliResponse = { type CompleteResponse = { base?: unknown; candidates?: unknown }; -const REMOTE_ERROR_BODY_MAX = 2000; - -/** Parse JSON from a remote CLI response; otherwise return a text preview (e.g. HTML 500 page). */ -async function readRemoteJsonBody(res: Response): Promise< - { kind: "json"; data: T } | { kind: "plain"; preview: string } -> { - const raw = await res.text(); - const t = raw.trim(); - if (t.length === 0) { - return { kind: "json", data: {} as T }; - } - try { - return { kind: "json", data: JSON.parse(t) as T }; - } catch { - const preview = - raw.length > REMOTE_ERROR_BODY_MAX ? raw.slice(0, REMOTE_ERROR_BODY_MAX) + "\n… (truncated)" : raw; - return { kind: "plain", preview }; - } -} - -const JSON_HEADERS = { - "Content-Type": "application/json", - Accept: "application/json", -} as const; - -/** JSON body for POST …/cli and …/cli/complete (`command` mirrors common server field names). */ -function remoteCliExecuteBody(line: string): string { - return JSON.stringify({ line, command: line }); -} - function formatErr(e: unknown): string { return e instanceof Error ? e.message : String(e); } @@ -185,12 +143,9 @@ function feedInput(chunk: string, carry: { s: string }): InputTok[] { export class KeeperShell extends HTMLElement { static observedAttributes = [ - ATTR_API_BASE, ATTR_KEEPER_HOST, ATTR_COLLAPSED, ATTR_HEIGHT, - ATTR_REMOTE, - ATTR_LOCAL, ATTR_EMBED, ATTR_MASK_INPUT, ]; @@ -204,6 +159,8 @@ export class KeeperShell extends HTMLElement { private _started = false; private _completing = false; private _inputListeners: AbortController | null = null; + /** Last `keeper-host` synced to {@link resetShellVault}; `undefined` = never synced. */ + private _syncedKeeperHostKey: string | undefined; constructor() { super(); @@ -211,18 +168,6 @@ export class KeeperShell extends HTMLElement { this.attachShadow({ mode: "open", delegatesFocus: true }); } - /** - * Base URL for CLI HTTP transport (no trailing slash). POST `${apiBase}/cli`, etc. - * Default `/api` when the attribute is omitted (legacy web-console behavior). - */ - get apiBase(): string { - return normalizeApiBase(this.getAttribute(ATTR_API_BASE)); - } - - set apiBase(v: string) { - this.setAttribute(ATTR_API_BASE, (v && v.trim()) || "/api"); - } - get keeperHost(): string { return (this.getAttribute(ATTR_KEEPER_HOST) || "").trim(); } @@ -250,31 +195,6 @@ export class KeeperShell extends HTMLElement { else this.removeAttribute(ATTR_MASK_INPUT); } - /** - * When set, CLI uses HTTP: POST `${apiBase}/cli`, etc. - * When omitted (default), input is handled in-browser (parser + Keeper SDK). - */ - get remote(): boolean { - return this.hasAttribute(ATTR_REMOTE); - } - - set remote(v: boolean) { - if (v) this.setAttribute(ATTR_REMOTE, ""); - else this.removeAttribute(ATTR_REMOTE); - } - - /** - * @deprecated Use {@link remote} instead. `local === !remote` (setting `local` removes `remote`). - */ - get local(): boolean { - return !this.remote; - } - - set local(v: boolean) { - if (v) this.removeAttribute(ATTR_REMOTE); - else this.setAttribute(ATTR_REMOTE, ""); - } - /** Browser CLI layout: terminal fills the element; no collapsible chrome. */ get embed(): boolean { return this.hasAttribute(ATTR_EMBED); @@ -285,24 +205,15 @@ export class KeeperShell extends HTMLElement { else this.removeAttribute(ATTR_EMBED); } - private _isLocal(): boolean { - return !this.hasAttribute(ATTR_REMOTE); - } - private _isEmbed(): boolean { return this.hasAttribute(ATTR_EMBED); } - /** `null` → in-browser CLI; else HTTP prefix (default `/api`). */ - private _remoteApiPrefix(): string | null { - if (this._isLocal()) return null; - return normalizeApiBase(this.getAttribute(ATTR_API_BASE)); - } - private _syncShellContext(): void { - const h = this.getAttribute(ATTR_KEEPER_HOST)?.trim(); + const h = this.getAttribute(ATTR_KEEPER_HOST)?.trim() || ""; setShellCliContext({ keeperHost: h || undefined }); - if (this._isLocal()) { + if (this._syncedKeeperHostKey !== h) { + this._syncedKeeperHostKey = h; resetShellVault(); } } @@ -315,8 +226,7 @@ export class KeeperShell extends HTMLElement { if (name === ATTR_KEEPER_HOST) { this._syncShellContext(); } - if (name === ATTR_EMBED || name === ATTR_LOCAL || name === ATTR_REMOTE) { - if (name === ATTR_LOCAL || name === ATTR_REMOTE) this._syncShellContext(); + if (name === ATTR_EMBED) { const shouldShow = this._isEmbed() || !this.collapsed; if (this._started) this._teardownTerminal(); this._renderShell(); @@ -325,18 +235,8 @@ export class KeeperShell extends HTMLElement { return; } if (name === ATTR_COLLAPSED && !this._isEmbed() && this.shadowRoot) { - const panel = this.shadowRoot.querySelector(".wc-panel"); - const btn = this.shadowRoot.querySelector(".wc-toggle"); - if (this.collapsed) { - panel?.setAttribute("hidden", ""); - if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, true); - this._teardownTerminal(); - } else { - panel?.removeAttribute("hidden"); - if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, false); - this._mountTerminal(); - } - this._applyHostShellMinHeight(); + if (this.collapsed) this._hidePanel(); + else this._showPanel(); } } @@ -359,6 +259,8 @@ export class KeeperShell extends HTMLElement { disconnectedCallback(): void { this._teardownTerminal(); + resetShellVault(); + this._syncedKeeperHostKey = undefined; } private _wireChrome(): void { @@ -370,23 +272,63 @@ export class KeeperShell extends HTMLElement { private _toggle(): void { if (this._isEmbed()) return; + const panel = this.shadowRoot?.querySelector(".wc-panel"); + if (!(panel instanceof HTMLElement)) return; + if (panel.hasAttribute("hidden")) { + this.removeAttribute(ATTR_COLLAPSED); + this._showPanel(); + } else { + this.setAttribute(ATTR_COLLAPSED, ""); + this._hidePanel(); + } + } + + /** Hide the terminal panel but keep xterm session (scrollback, history, current line). */ + private _hidePanel(): void { const panel = this.shadowRoot?.querySelector(".wc-panel"); const btn = this.shadowRoot?.querySelector(".wc-toggle"); - if (!(panel instanceof HTMLElement) || !(btn instanceof HTMLButtonElement)) return; - const hidden = panel.hasAttribute("hidden"); - if (hidden) { - panel.removeAttribute("hidden"); - setConsoleToggleButtonUi(btn, false); + if (panel instanceof HTMLElement) panel.setAttribute("hidden", ""); + if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, true); + if (this._term) this._term.options.cursorBlink = false; + this._applyHostShellMinHeight(); + } + + /** Show the panel; mount on first open, otherwise refit and focus the existing terminal. */ + private _showPanel(): void { + const panel = this.shadowRoot?.querySelector(".wc-panel"); + const btn = this.shadowRoot?.querySelector(".wc-toggle"); + if (panel instanceof HTMLElement) panel.removeAttribute("hidden"); + if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, false); + if (!this._started) { this._mountTerminal(); } else { - panel.setAttribute("hidden", ""); - setConsoleToggleButtonUi(btn, true); - this._teardownTerminal(); + this._resumeTerminal(); } this._applyHostShellMinHeight(); } - /** When the panel is hidden, avoid reserving terminal height so the page stays compact. */ + private _resumeTerminal(): void { + const term = this._term; + const fit = this._fit; + if (!term || !fit) return; + term.options.cursorBlink = true; + const refit = (): void => { + try { + fit.fit(); + if (term.cols < 2 || term.rows < 1) { + term.resize(80, 24); + } + term.focus(); + } catch { + /* detached or zero-size host */ + } + }; + requestAnimationFrame(() => { + refit(); + requestAnimationFrame(refit); + }); + } + private _applyHostShellMinHeight(): void { if (this._isEmbed()) return; const panel = this.shadowRoot?.querySelector(".wc-panel"); @@ -584,9 +526,7 @@ export class KeeperShell extends HTMLElement { const cap = document.createElement("div"); cap.className = "wc-cli-cap"; - cap.textContent = this.remote - ? `Keeper CLI — HTTP (${this.apiBase})` - : "Keeper CLI — in-browser SDK (click in the terminal, then type)"; + cap.textContent = "Keeper CLI — in-browser SDK (click in the terminal, then type)"; const host = document.createElement("div"); host.className = "wc-terminal-host"; @@ -804,28 +744,7 @@ export class KeeperShell extends HTMLElement { this._completing = true; try { bumpEditing(); - const remote = this._remoteApiPrefix(); - let data: CompleteResponse; - if (remote === null) { - data = completeCliLine(this._lineBuf) as CompleteResponse; - } else { - const url = `${remote}/cli/complete`; - const res = await fetch(url, { - method: "POST", - headers: JSON_HEADERS, - body: remoteCliExecuteBody(this._lineBuf), - }); - const parsed = await readRemoteJsonBody(res); - if (parsed.kind === "plain") { - term.write("\x07"); - return; - } - data = parsed.data; - if (!res.ok) { - term.write("\x07"); - return; - } - } + const data = completeCliLine(this._lineBuf) as CompleteResponse; const base = typeof data.base === "string" ? data.base : ""; const raw = data.candidates; const candidates = Array.isArray(raw) @@ -878,38 +797,7 @@ export class KeeperShell extends HTMLElement { pendingLoginUsername = null; term.writeln("\x1b[90mSigning in…\x1b[0m"); try { - const remote = this._remoteApiPrefix(); - let data: CliResponse; - if (remote === null) { - data = await loginWithCredentials(username, cmd); - } else { - const res = await fetch(`${remote}/cli/login`, { - method: "POST", - headers: JSON_HEADERS, - body: JSON.stringify({ username, password: cmd }), - }); - const parsed = await readRemoteJsonBody(res); - if (parsed.kind === "plain") { - term.write( - `\x1b[31mHTTP ${res.status}: response is not JSON (server error page or non-API response).\n${parsed.preview}\x1b[0m\n` - ); - resetMaskAfterPasswordFlow(); - writeFreshPrompt(); - return; - } - data = parsed.data; - if (!res.ok) { - const d = data as CliResponse & { message?: string }; - const parts = [d?.error, d?.err, d?.out, d?.message].filter( - (x): x is string => typeof x === "string" && x.trim().length > 0 - ); - const msg = parts.length > 0 ? parts.join("\n") : res.statusText || "request failed"; - term.write(`\x1b[31m${msg}\x1b[0m\n`); - resetMaskAfterPasswordFlow(); - writeFreshPrompt(); - return; - } - } + const data = await loginWithCredentials(username, cmd); if (data.out) term.write(data.out); if (data.err) term.write(`\x1b[31m${data.err}\x1b[0m`); } catch (err) { @@ -927,36 +815,7 @@ export class KeeperShell extends HTMLElement { if (!skipHistory) pushHistoryEntry(cmd); try { - const remote = this._remoteApiPrefix(); - let data: CliResponse; - if (remote === null) { - data = await dispatchCliLine(cmd); - } else { - const res = await fetch(`${remote}/cli`, { - method: "POST", - headers: JSON_HEADERS, - body: remoteCliExecuteBody(cmd), - }); - const parsed = await readRemoteJsonBody(res); - if (parsed.kind === "plain") { - term.write( - `\x1b[31mHTTP ${res.status}: response is not JSON (server error page or non-API response).\n${parsed.preview}\x1b[0m\n` - ); - writeFreshPrompt(); - return; - } - data = parsed.data; - if (!res.ok) { - const d = data as CliResponse & { message?: string }; - const parts = [d?.error, d?.err, d?.out, d?.message].filter( - (x): x is string => typeof x === "string" && x.trim().length > 0 - ); - const msg = parts.length > 0 ? parts.join("\n") : res.statusText || "request failed"; - term.write(`\x1b[31m${msg}\x1b[0m\n`); - writeFreshPrompt(); - return; - } - } + const data = await dispatchCliLine(cmd); if (data.needPassword === true && typeof data.loginUsername === "string") { pendingLoginUsername = data.loginUsername; maskSensitive = true; @@ -1083,28 +942,15 @@ export class KeeperShell extends HTMLElement { this._chain = this._chain.then(() => handleDataChunk(data)); }); - const remote = this._remoteApiPrefix(); - if (remote === null) { - term.writeln("\x1b[1mWeb console\x1b[0m — commands run in this browser (Keeper SDK + CLI)."); - term.writeln( - "Type `help` — records, folders, shared-folders, teams, users, vault summary (see each COMMAND --help)." - ); - term.writeln("Tab completion and masked password entry are handled locally."); - term.writeln("Optional: `keeper-host` attribute (or VITE_KEEPER_HOST) for vault region."); - } else { - term.writeln("\x1b[1mWeb console\x1b[0m — commands execute on your backend API."); - term.writeln( - "Transport: JSON POST { line, command } (same string) to " + `${this.apiBase}/cli` - ); - term.writeln( - "Tab completes commands (POST { line, command } to " + `${this.apiBase}/cli/complete).` - ); - } + term.writeln("\x1b[1mWeb console\x1b[0m — commands run in this browser (Keeper SDK + CLI)."); + term.writeln( + "Type `help` — records, folders, shared-folders, teams, users, vault summary (see each COMMAND --help)." + ); + term.writeln("Tab completion and masked password entry are handled locally."); + term.writeln("Optional: `keeper-host` attribute (or VITE_KEEPER_HOST) for vault region."); term.writeln("Up / Down — history; Left / Right — move cursor; Delete — forward delete."); term.writeln( - remote === null - ? "Ctrl+O — toggle masked input (* per character; processed locally; masked lines are not saved to history)." - : "Ctrl+O — toggle masked input (* per character; real text is sent to the API; masked lines are not saved to history)." + "Ctrl+O — toggle masked input (* per character; processed locally; masked lines are not saved to history)." ); writeFreshPrompt(); term.focus(); diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts index 9006f46b..6ce0ba19 100644 --- a/shellcomponent/src/cli/keeperCliHost.ts +++ b/shellcomponent/src/cli/keeperCliHost.ts @@ -66,7 +66,7 @@ function formatKeeperClientError(context: string, e: unknown): string { `${context}: ${base}\n` + ` Browser could not reach Keeper (CORS, network, or wrong region).` + hostHint + - `\n Dev: run shellcomponent with \`npm run dev\` (same-origin proxy). Prod: set keeper-host or use remote + api-base.\n` + `\n Dev: run shellcomponent with \`npm run dev\` (same-origin proxy). Prod: set keeper-host on the element or host-page proxy for Keeper API.\n` ); } @@ -104,49 +104,68 @@ function asCliVault(v: VaultInstance): KeeperCliVault { }; } +function isHtmlLike(text: string): boolean { + const head = text.trimStart().slice(0, 32).toLowerCase(); + return head.startsWith(" { const p = path.trim().replace(/^@/, ""); + if (/^https?:\/\//i.test(p)) { const res = await fetch(p); if (!res.ok) { throw new Error(`HTTP ${res.status} loading ${p}`); } - return res.text(); + const text = await res.text(); + if (isHtmlLike(text)) { + throw new Error(`URL returned HTML, not JSON: ${p}`); + } + return text; } + const isDev = import.meta.env?.DEV === true; + const tryFetch = async (url: string): Promise => { const res = await fetch(url); if (!res.ok) return null; const ct = res.headers.get("content-type") ?? ""; if (ct.includes("text/html")) return null; const text = await res.text(); - const head = text.trimStart().slice(0, 32).toLowerCase(); - if (head.startsWith(" { const el = node as HTMLElement; while (el.firstChild) el.removeChild(el.firstChild); const pre = document.createElement("pre"); pre.style.cssText = "display:block;margin:0;padding:16px;background:#2a0a0a;color:#fecaca;border:2px solid #991b1b;border-radius:8px;white-space:pre-wrap;font:13px/1.45 ui-monospace,monospace;"; - pre.textContent = `Keeper shell failed to load:\n\n${msg}\n\n${hint}`; + pre.textContent = `Keeper shell failed to load:\n\n${msg}\n`; el.appendChild(pre); el.style.display = "block"; el.style.width = "100%"; diff --git a/shellcomponent/vite.config.ts b/shellcomponent/vite.config.ts index b0a7ee9a..eb651df7 100644 --- a/shellcomponent/vite.config.ts +++ b/shellcomponent/vite.config.ts @@ -29,61 +29,70 @@ function keeperDevSessionConfPlugin(): Plugin { }; } -export default defineConfig({ - root: ".", - plugins: [keeperDevSessionConfPlugin(), keeperSameOriginProxyPlugin()], - resolve: { - alias: { - "@keeper-security/keeper-sdk-javascript": resolve(__dirname, "../KeeperSdk/src/browser.ts"), - "fs/promises": resolve(__dirname, "src/shims/fs-promises-empty.ts"), - fs: resolve(__dirname, "src/shims/fs-empty.ts"), - path: resolve(__dirname, "node_modules/path-browserify/index.js"), - os: resolve(__dirname, "src/shims/os-homedir.ts"), - buffer: "buffer/", - }, - }, - define: { - global: "globalThis", - }, - optimizeDeps: { - include: [ - "@keeper-security/keeper-sdk-javascript", - "@keeper-security/keeperapi", - "@xterm/xterm", - "@xterm/addon-fit", - "buffer", - "protobufjs", - ], - esbuildOptions: { - define: { - global: "globalThis", +export default defineConfig(({ command, mode }) => { + const isServe = command === "serve"; + const isProd = mode === "production"; + + return { + root: ".", + plugins: isServe ? [keeperDevSessionConfPlugin(), keeperSameOriginProxyPlugin()] : [], + resolve: { + alias: { + // Bundle from KeeperSdk sources (CJS dist/browser.js breaks Rollup named exports). + "@keeper-security/keeper-sdk-javascript": resolve( + __dirname, + "../KeeperSdk/src/browser.ts" + ), + "fs/promises": resolve(__dirname, "src/shims/fs-promises-empty.ts"), + fs: resolve(__dirname, "src/shims/fs-empty.ts"), + path: resolve(__dirname, "node_modules/path-browserify/index.js"), + os: resolve(__dirname, "src/shims/os-homedir.ts"), + buffer: "buffer/", }, }, - }, - build: { - target: "es2017", - commonjsOptions: { - transformMixedEsModules: true, + define: { + global: "globalThis", }, - lib: { - entry: resolve(__dirname, "src/index.ts"), - name: "KeeperShell", - formats: ["es", "umd"], - fileName: (format) => - format === "umd" ? "keeper-shell.umd.cjs" : "keeper-shell.es.js", + optimizeDeps: { + include: [ + "@keeper-security/keeper-sdk-javascript", + "@keeper-security/keeperapi", + "@xterm/xterm", + "@xterm/addon-fit", + "buffer", + "protobufjs", + ], + esbuildOptions: { + define: { + global: "globalThis", + }, + }, }, - rollupOptions: { - output: { - exports: "named", - assetFileNames: "keeper-shell.[ext]", + build: { + target: "es2017", + sourcemap: isProd ? false : true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "KeeperShell", + formats: ["es", "umd"], + fileName: (format) => + format === "umd" ? "keeper-shell.umd.cjs" : "keeper-shell.es.js", + }, + rollupOptions: { + output: { + exports: "named", + assetFileNames: "keeper-shell.[ext]", + }, }, }, - sourcemap: true, - }, - server: { - port: 5175, - fs: { - allow: [__dirname, resolve(__dirname, "..")], + server: { + port: 5175, + fs: { + allow: [__dirname, resolve(__dirname, "..")], + }, }, - }, + }; }); From ce2beb731e858bf3212dea17cdd06d5a94c7b20e Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 1 Jun 2026 18:26:32 +0530 Subject: [PATCH 13/23] added packagejson --- shellcomponent/package-lock.json | 10 ++++++---- shellcomponent/package.json | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/shellcomponent/package-lock.json b/shellcomponent/package-lock.json index 223a3594..3f98f6c4 100644 --- a/shellcomponent/package-lock.json +++ b/shellcomponent/package-lock.json @@ -27,7 +27,7 @@ }, "../keeperapi": { "name": "@keeper-security/keeperapi", - "version": "17.1.3", + "version": "17.2.2", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", @@ -46,10 +46,10 @@ "jest": "^29.6.1", "jest-environment-jsdom": "^29.7.0", "prettier": "^3.8.1", - "protobufjs": "^7.2.4", - "protobufjs-cli": "^1.1.1", + "protobufjs": "^7.6.1", + "protobufjs-cli": "^1.3.1", "rollup": "^2.79.2", - "rollup-plugin-typescript2": "^0.32.1", + "rollup-plugin-typescript2": "^0.37.0", "ts-jest": "^29.1.1", "ts-node": "^8.10.2", "typescript": "^4.0.1" @@ -58,9 +58,11 @@ "../KeeperSdk": { "name": "@keeper-security/keeper-sdk-javascript", "version": "1.0.0", + "license": "ISC", "dependencies": { "@keeper-security/keeperapi": "file:../keeperapi", "asmcrypto.js": "^2.3.2", + "ts-node": "^10.7.0", "typescript": "^4.6.3" }, "devDependencies": { diff --git a/shellcomponent/package.json b/shellcomponent/package.json index b83e118b..63469686 100644 --- a/shellcomponent/package.json +++ b/shellcomponent/package.json @@ -1,7 +1,7 @@ { "name": "@keeper-security/keeper-shell-component", "version": "1.0.0", - "description": "Keeper web console: / + CLI + Keeper JavaScript SDK (in-browser); optional remote + api-base for your own HTTP relay.", + "description": "Keeper web console: / + CLI + Keeper JavaScript SDK (in-browser).", "type": "module", "main": "./dist/keeper-shell.es.js", "module": "./dist/keeper-shell.es.js", @@ -19,11 +19,13 @@ ], "scripts": { "clean": "rm -rf dist node_modules", - "rebuild": "npm run clean && npm install && npm --prefix ../KeeperSdk run rebuild && npm run build", + "rebuild": "npm run clean && npm install && npm run build:deps && npm run build", + "build:deps": "npm --prefix ../keeperapi run build && npm --prefix ../KeeperSdk run build", "build:local-keeper-sdk": "npm --prefix ../KeeperSdk install && npm --prefix ../KeeperSdk run build", "dev": "npm install && vite", "dev:inspect": "npm install && node --inspect=9229 ./node_modules/vite/bin/vite.js", - "build": "vite build", + "build": "npm run build:deps && vite build", + "prepublishOnly": "npm run build", "preview": "vite preview", "typecheck": "tsc --noEmit" }, From 608bacb1080b621c6a1f8633423ca0686678311d Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 8 Jun 2026 14:21:30 +0530 Subject: [PATCH 14/23] cli refactor --- KeeperSdk/src/api.ts | 7 + KeeperSdk/src/cli/access.ts | 31 +++ KeeperSdk/src/cli/builtinCommands.ts | 47 ++++ KeeperSdk/src/cli/commander/get.ts | 41 +++ KeeperSdk/src/cli/commander/getCore.ts | 153 ++++++++++++ KeeperSdk/src/cli/commander/index.ts | 28 +++ KeeperSdk/src/cli/commander/misc.ts | 185 ++++++++++++++ KeeperSdk/src/cli/commander/nav.ts | 233 ++++++++++++++++++ KeeperSdk/src/cli/commands/help.ts | 44 +++- KeeperSdk/src/cli/commands/records.ts | 25 +- KeeperSdk/src/cli/commands/sync.ts | 2 +- KeeperSdk/src/cli/dispatch.ts | 7 + KeeperSdk/src/cli/help.ts | 17 +- KeeperSdk/src/cli/index.ts | 39 ++- KeeperSdk/src/cli/parser.ts | 65 +++-- KeeperSdk/src/cli/prompt.ts | 12 + KeeperSdk/src/cli/types.ts | 1 + KeeperSdk/src/records/RecordUtils.ts | 68 ++++- KeeperSdk/src/vault/KeeperVault.ts | 4 + .../sdk_example/src/utils/shellCliHost.ts | 1 + shellcomponent/.gitignore | 1 + shellcomponent/index.html | 1 - shellcomponent/keeper-shell.d.ts | 5 +- shellcomponent/src/KeeperShell.ts | 34 ++- shellcomponent/src/cli/cliComplete.ts | 16 +- shellcomponent/src/cli/keeperCliHost.ts | 1 + 26 files changed, 958 insertions(+), 110 deletions(-) create mode 100644 KeeperSdk/src/cli/access.ts create mode 100644 KeeperSdk/src/cli/builtinCommands.ts create mode 100644 KeeperSdk/src/cli/commander/get.ts create mode 100644 KeeperSdk/src/cli/commander/getCore.ts create mode 100644 KeeperSdk/src/cli/commander/index.ts create mode 100644 KeeperSdk/src/cli/commander/misc.ts create mode 100644 KeeperSdk/src/cli/commander/nav.ts create mode 100644 KeeperSdk/src/cli/prompt.ts diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts index cc450b95..d8e1583b 100644 --- a/KeeperSdk/src/api.ts +++ b/KeeperSdk/src/api.ts @@ -190,6 +190,9 @@ export { getCliCommand, listCliCommands, listCliCommandNames, + listCliCommandNamesForLoginState, + listCliCommandsForLoginState, + isAuthCliCommand, listDocumentedCommands, getDetailedHelpPage, formatDetailedHelpForCommand, @@ -203,6 +206,10 @@ export { runLogoutCommand, KeeperCliParser, createKeeperCliParser, + getKeeperCliPromptPrefix, + BUILTIN_CLI_COMMANDS, + registerBuiltinCliCommands, + registerCommanderCliCommands, } from './cli' export type { KeeperCliParserOptions } from './cli' export type { diff --git a/KeeperSdk/src/cli/access.ts b/KeeperSdk/src/cli/access.ts new file mode 100644 index 00000000..1f6f1ebf --- /dev/null +++ b/KeeperSdk/src/cli/access.ts @@ -0,0 +1,31 @@ +import type { CliCommandDefinition } from './types' +import { listCliCommands, resolveCliCommandName } from './registry' + +/** Commands available before a vault session exists. */ +export const AUTH_CLI_COMMAND_NAMES = new Set([ + 'help', + 'login', + 'restore-session', + 'register-device', +]) + +export function isAuthCliCommand(name: string): boolean { + const resolved = resolveCliCommandName(name) + return resolved != null && AUTH_CLI_COMMAND_NAMES.has(resolved) +} + +export function filterCliCommandsForLoginState( + commands: readonly CliCommandDefinition[], + loggedIn: boolean +): CliCommandDefinition[] { + if (loggedIn) return [...commands] + return commands.filter((c) => AUTH_CLI_COMMAND_NAMES.has(c.name)) +} + +export function listCliCommandsForLoginState(loggedIn: boolean): CliCommandDefinition[] { + return filterCliCommandsForLoginState(listCliCommands(), loggedIn) +} + +export function listCliCommandNamesForLoginState(loggedIn: boolean): readonly string[] { + return listCliCommandsForLoginState(loggedIn).map((c) => c.name) +} diff --git a/KeeperSdk/src/cli/builtinCommands.ts b/KeeperSdk/src/cli/builtinCommands.ts new file mode 100644 index 00000000..88cb2917 --- /dev/null +++ b/KeeperSdk/src/cli/builtinCommands.ts @@ -0,0 +1,47 @@ +import type { CliCommandDefinition } from './types' +import { registerCliCommand } from './registry' +import { foldersCommand } from './commands/folders' +import { helpCommand } from './commands/help' +import { loginCommand } from './commands/login' +import { logoutCommand } from './commands/logout' +import { recordsCommand } from './commands/records' +import { registerDeviceCommand } from './commands/registerDevice' +import { restoreSessionCommand } from './commands/restoreSession' +import { sharedFoldersCommand } from './commands/sharedFolders' +import { syncCommand } from './commands/sync' +import { teamsCommand } from './commands/teams' +import { usersCommand } from './commands/users' +import { vaultCommand } from './commands/vault' +import { getCommand } from './commander/get' +import { cdCommand, lsCommand, mkdirCommand, treeCommand } from './commander/nav' +import { listSfCommand, listTeamCommand, searchCommand, whoamiCommand } from './commander/misc' + +export const BUILTIN_CLI_COMMANDS: readonly CliCommandDefinition[] = [ + helpCommand, + loginCommand, + registerDeviceCommand, + restoreSessionCommand, + syncCommand, + vaultCommand, + getCommand, + lsCommand, + cdCommand, + treeCommand, + mkdirCommand, + searchCommand, + listSfCommand, + listTeamCommand, + whoamiCommand, + recordsCommand, + foldersCommand, + sharedFoldersCommand, + teamsCommand, + usersCommand, + logoutCommand, +] + +export function registerBuiltinCliCommands(): void { + for (const def of BUILTIN_CLI_COMMANDS) { + registerCliCommand(def) + } +} diff --git a/KeeperSdk/src/cli/commander/get.ts b/KeeperSdk/src/cli/commander/get.ts new file mode 100644 index 00000000..e31a19bf --- /dev/null +++ b/KeeperSdk/src/cli/commander/get.ts @@ -0,0 +1,41 @@ +import type { CliCommandDefinition } from '../types' +import { wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { executeGet } from './getCore' + +export const getCommand: CliCommandDefinition = { + name: 'get', + order: 10, + aliases: ['g'], + description: 'Get details of a record, folder, or team by UID or title.', + usage: 'get [--format {detail,json,password,fields}] [--unmask]', + flagOptions: ['--format', '--unmask', '--detail', '--json'], + help: { + title: 'get — record/folder/team details (Keeper Commander)', + synopsis: 'usage: get [--unmask] [--format {detail,json,password,fields}] uid', + description: + ' Resolves a vault object by UID or title. Records support all output formats; folders and teams support detail/json.', + arguments: ' uid Record, folder, or team UID or title.', + options: ` --format {detail,json,password,fields} + detail (default): human-readable output. + json: JSON object. + password: password field only (records). + fields: JSON array of {name, value} (records). + --unmask Show sensitive field values (records). + --help, -h Show this help.`, + examples: ` get "Amazon" + get AbCdEf123456 --format json --unmask + get MyFolderUid --format json`, + seeAlso: ' ls, search, record-update', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) { + return { code: 0, out: formatDetailedHelpForCommand(getCommand), err: '' } + } + try { + return await executeGet(host, parsed, 'get') + } catch (e) { + return { code: 1, out: '', err: host.formatError('get', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commander/getCore.ts b/KeeperSdk/src/cli/commander/getCore.ts new file mode 100644 index 00000000..ceec4a65 --- /dev/null +++ b/KeeperSdk/src/cli/commander/getCore.ts @@ -0,0 +1,153 @@ +import type { DRecord } from '@keeper-security/keeperapi' +import { + formatRecord, + formatRecordFields, + getRecordPassword, + getRecordTitle, +} from '../../records/RecordUtils' +import { formatTeamView, teamViewTable } from '../../teams/viewTeam' +import type { CliResult, KeeperCliHost, ParsedCli } from '../types' +import { getOpt, hasOpt } from '../parse' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { GetFolderFormat } from '../../folders/getFolder' + +export type GetOutputFormat = 'detail' | 'json' | 'password' | 'fields' + +export function resolveGetFormat(parsed: ParsedCli): GetOutputFormat { + const raw = getOpt(parsed.opts, 'format')?.toLowerCase() + if (raw === 'json' || hasOpt(parsed.opts, 'json')) return 'json' + if (raw === 'password') return 'password' + if (raw === 'fields') return 'fields' + if (raw === 'detail') return 'detail' + return hasOpt(parsed.opts, 'detail') ? 'detail' : 'detail' +} + +export function resolveGetUnmask(parsed: ParsedCli): boolean { + return hasOpt(parsed.opts, 'unmask') +} + +export function getGetTarget(parsed: ParsedCli): string | undefined { + return parsed.positional[0]?.trim() || undefined +} + +async function outputRecord( + host: KeeperCliHost, + record: DRecord, + fmt: GetOutputFormat, + unmask: boolean, + cmd: string +): Promise { + if (fmt === 'password') { + const pw = getRecordPassword(record) + return { code: 0, out: pw ? `${pw}\n` : '', err: pw ? '' : `${cmd}: record has no password field\n` } + } + if (fmt === 'fields') { + return { code: 0, out: JSON.stringify(formatRecordFields(record, unmask), null, 2) + '\n', err: '' } + } + if (fmt === 'json') { + return { code: 0, out: JSON.stringify(record, null, 2) + '\n', err: '' } + } + return { code: 0, out: formatRecord(record, { showDetails: true, unmask }) + '\n', err: '' } +} + +async function tryGetFolder( + host: KeeperCliHost, + target: string, + fmt: GetOutputFormat, + cmd: string +): Promise { + const v = host.getVault() + if (!v.getFolder) return null + try { + const res = await v.getFolder(target, { + format: fmt === 'json' ? GetFolderFormat.JSON : GetFolderFormat.Detail, + }) + if (fmt === 'json') { + const json = (res as { json?: Record }).json ?? res + return { code: 0, out: JSON.stringify(json, null, 2) + '\n', err: '' } + } + const name = 'name' in res ? res.name : target + const uid = + 'folder_uid' in res + ? res.folder_uid + : 'shared_folder_uid' in res + ? res.shared_folder_uid + : target + return { code: 0, out: `${name}\t${uid}\n`, err: '' } + } catch { + return null + } +} + +async function tryGetTeam( + host: KeeperCliHost, + target: string, + fmt: GetOutputFormat, + cmd: string +): Promise { + const v = host.getVault() + if (!v.viewTeam) return null + try { + const view = await v.viewTeam(target) + if (fmt === 'json') { + return { code: 0, out: JSON.stringify(view, null, 2) + '\n', err: '' } + } + if (fmt === 'password' || fmt === 'fields') { + return { code: 1, out: '', err: `${cmd}: --format ${fmt} applies to records only\n` } + } + const formatted = formatTeamView(view, { verbose: true }) + return { code: 0, out: teamViewTable(formatted) + '\n', err: '' } + } catch { + return null + } +} + +async function tryGetSharedFolderByUid( + host: KeeperCliHost, + target: string, + fmt: GetOutputFormat, + cmd: string +): Promise { + const v = host.getVault() + const hit = v.getSharedFolders().find((sf) => sf.uid === target) + if (!hit) return null + if (fmt === 'json') { + return { code: 0, out: JSON.stringify(hit, null, 2) + '\n', err: '' } + } + if (fmt === 'password' || fmt === 'fields') { + return { code: 1, out: '', err: `${cmd}: --format ${fmt} applies to records only\n` } + } + return { code: 0, out: `${hit.name ?? '(unnamed)'}\t${hit.uid}\n`, err: '' } +} + +/** Commander-style `get` (record, folder, team, or shared folder by UID/title). */ +export async function executeGet(host: KeeperCliHost, parsed: ParsedCli, cmd = 'get'): Promise { + const target = getGetTarget(parsed) + if (!target) { + return { code: 1, out: '', err: `${cmd}: UID parameter is required\n` } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const fmt = resolveGetFormat(parsed) + const unmask = resolveGetUnmask(parsed) + + await v.sync() + + const sf = await tryGetSharedFolderByUid(host, target, fmt, cmd) + if (sf) return sf + + const folder = await tryGetFolder(host, target, fmt, cmd) + if (folder) return folder + + const team = await tryGetTeam(host, target, fmt, cmd) + if (team) return team + + const cap = ensureCapability(v, 'findRecord', cmd) + if (cap) return cap + const record = v.findRecord!(target) + if (!record) { + return { code: 1, out: '', err: `${cmd}: cannot find any object matching "${target}"\n` } + } + return outputRecord(host, record, fmt, unmask, cmd) +} diff --git a/KeeperSdk/src/cli/commander/index.ts b/KeeperSdk/src/cli/commander/index.ts new file mode 100644 index 00000000..6461335e --- /dev/null +++ b/KeeperSdk/src/cli/commander/index.ts @@ -0,0 +1,28 @@ +import { registerCliCommand } from '../registry' +import { getCommand } from './get' +import { cdCommand, lsCommand, mkdirCommand, treeCommand } from './nav' +import { listSfCommand, listTeamCommand, searchCommand, whoamiCommand } from './misc' + +export { getCommand } from './get' +export { executeGet } from './getCore' +export { lsCommand, cdCommand, treeCommand, mkdirCommand } from './nav' +export { searchCommand, listSfCommand, listTeamCommand, whoamiCommand } from './misc' + +const COMMANDER_COMMANDS = [ + getCommand, + lsCommand, + cdCommand, + treeCommand, + mkdirCommand, + searchCommand, + listSfCommand, + listTeamCommand, + whoamiCommand, +] + +/** Register top-level Keeper Commander-style vault commands (JS SDK under the hood). */ +export function registerCommanderCliCommands(): void { + for (const def of COMMANDER_COMMANDS) { + registerCliCommand(def) + } +} diff --git a/KeeperSdk/src/cli/commander/misc.ts b/KeeperSdk/src/cli/commander/misc.ts new file mode 100644 index 00000000..3f6b1dfd --- /dev/null +++ b/KeeperSdk/src/cli/commander/misc.ts @@ -0,0 +1,185 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { getOpt, hasOpt, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { formatTable } from '../table' +import { getRecordTitle } from '../../records/RecordUtils' +import { recordUid } from '../utils' +import { formatSharedFoldersTable, renderSharedFoldersAsciiTable } from '../../sharedFolders/listSharedFolders' +import { formatTeamsTable, renderTeamsAsciiTable } from '../../teams/listTeams' + +async function runSearch(host: KeeperCliHost, parsed: ParsedCli): Promise { + const pattern = parsed.positional.join(' ') || getOpt(parsed.opts, 'pattern') + if (!pattern?.trim()) { + return { code: 1, out: '', err: 'search: missing search terms. Usage: search \n' } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'findRecords', 'search') + if (cap) return cap + await v.sync() + const matches = v.findRecords!(pattern) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(matches, null, 2) + '\n', err: '' } + } + if (matches.length === 0) { + return { code: 0, out: `(no records matched "${pattern}")\n`, err: '' } + } + const rows = matches.map((rec) => [recordUid(rec), getRecordTitle(rec)]) + return { code: 0, out: formatTable(['record_uid', 'title'], rows), err: '' } +} + +async function runListSf(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listSharedFolders', 'list-sf') + if (cap) return cap + await v.sync() + const pattern = parsed.positional[0] ?? getOpt(parsed.opts, 'pattern') ?? null + const verbose = hasOpt(parsed.opts, 'verbose') || hasOpt(parsed.opts, 'v') + const rows = v.listSharedFolders!({ pattern, verbose, includeDetails: verbose }) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } + } + if (rows.length === 0) { + return { + code: 0, + out: pattern ? `(no shared folders matched "${pattern}")\n` : '(no shared folders)\n', + err: '', + } + } + const table = formatSharedFoldersTable(rows, { verbose }) + return { code: 0, out: renderSharedFoldersAsciiTable(table) + '\n', err: '' } +} + +async function runListTeam(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listTeams', 'list-team') + if (cap) return cap + await v.sync() + const pattern = parsed.positional[0] ?? getOpt(parsed.opts, 'pattern') ?? null + const rows = await v.listTeams!({ pattern }) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } + } + if (rows.length === 0) { + return { code: 0, out: pattern ? `(no teams matched "${pattern}")\n` : '(no teams)\n', err: '' } + } + const table = formatTeamsTable(rows) + return { code: 0, out: renderTeamsAsciiTable(table) + '\n', err: '' } +} + +async function runWhoami(host: KeeperCliHost): Promise { + const v = host.getVault() + if (!v.isLoggedIn) { + return { code: 1, out: '', err: 'whoami: not logged in\n' } + } + const username = + (await host.getAccountUsername?.()) ?? host.envString('KEEPER_USER') ?? host.envString('KEEPER_USERNAME') + const summary = v.getSummary?.() + const lines = [`username: ${username ?? '(unknown)'}`] + if (summary) { + lines.push( + `records: ${summary.recordCount}`, + `folders: ${summary.folderCount}`, + `shared_folders: ${summary.sharedFolderCount}`, + `teams: ${summary.teamCount}` + ) + } + return { code: 0, out: lines.join('\n') + '\n', err: '' } +} + +export const searchCommand: CliCommandDefinition = { + name: 'search', + order: 15, + aliases: ['s'], + description: 'Search vault records by text.', + usage: 'search [--json]', + flagOptions: ['--json', '--pattern'], + help: { + title: 'search — find records (Keeper Commander)', + synopsis: 'usage: search ', + description: ' Space-separated terms; all terms must match somewhere in the record.', + examples: ' search amazon\n search bank account', + seeAlso: ' get, ls', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(searchCommand), err: '' } + try { + return await runSearch(host, parsed) + } catch (e) { + return { code: 1, out: '', err: host.formatError('search', e) } + } + }, +} + +export const listSfCommand: CliCommandDefinition = { + name: 'list-sf', + order: 16, + aliases: ['lsf'], + description: 'List shared folders.', + usage: 'list-sf [pattern] [--verbose] [--json]', + flagOptions: ['--verbose', '-v', '--json', '--pattern'], + help: { + title: 'list-sf — shared folders (Keeper Commander)', + synopsis: 'usage: list-sf [pattern]', + seeAlso: ' ls, get', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(listSfCommand), err: '' } + try { + return await runListSf(host, parsed) + } catch (e) { + return { code: 1, out: '', err: host.formatError('list-sf', e) } + } + }, +} + +export const listTeamCommand: CliCommandDefinition = { + name: 'list-team', + order: 17, + aliases: ['lt'], + description: 'List teams.', + usage: 'list-team [pattern] [--json]', + flagOptions: ['--json', '--pattern'], + help: { + title: 'list-team — teams (Keeper Commander)', + synopsis: 'usage: list-team [pattern]', + seeAlso: ' get, whoami', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(listTeamCommand), err: '' } + try { + return await runListTeam(host, parsed) + } catch (e) { + return { code: 1, out: '', err: host.formatError('list-team', e) } + } + }, +} + +export const whoamiCommand: CliCommandDefinition = { + name: 'whoami', + order: 18, + description: 'Display current user and vault counts.', + usage: 'whoami', + help: { + title: 'whoami — current user (Keeper Commander)', + synopsis: 'usage: whoami', + seeAlso: ' login, sync-down', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(whoamiCommand), err: '' } + if (parsed.opts.size > 0 || parsed.positional.length > 0) { + return { code: 1, out: '', err: 'whoami: unexpected arguments\n' } + } + try { + return await runWhoami(host) + } catch (e) { + return { code: 1, out: '', err: host.formatError('whoami', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commander/nav.ts b/KeeperSdk/src/cli/commander/nav.ts new file mode 100644 index 00000000..7d59ee01 --- /dev/null +++ b/KeeperSdk/src/cli/commander/nav.ts @@ -0,0 +1,233 @@ +import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' +import { getOpt, hasOpt, wantsCliHelp } from '../parse' +import { formatDetailedHelpForCommand } from '../help' +import { ensureCapability, ensureSession } from '../commandHelpers' +import { formatTable } from '../table' + +function lsPath(parsed: ParsedCli): string | undefined { + return parsed.positional[0] +} + +function formatLs( + result: { + detail: boolean + folders: Array<{ uid: string; name: string }> + records: Array<{ uid: string; name: string; type?: string }> + }, + detail: boolean +): string { + if (result.folders.length + result.records.length === 0) return '(empty)\n' + + const headers = detail ? ['flags', 'uid', 'name', 'type'] : ['kind', 'uid', 'name'] + const rows: string[][] = [] + for (const f of result.folders) { + const flags = ((f as { flags?: string }).flags ?? '').trim() + rows.push(detail ? [flags || 'f---', f.uid, f.name, ''] : ['dir', f.uid, f.name]) + } + for (const r of result.records) { + const flags = ((r as { flags?: string }).flags ?? '').trim() + const type = r.type ?? '' + rows.push(detail ? [flags || 'r---', r.uid, r.name, type] : ['rec', r.uid, r.name]) + } + return formatTable(headers, rows) +} + +async function runLs(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'listFolder', cmd) + if (cap) return cap + await v.sync() + + const detail = hasOpt(parsed.opts, 'detail') || hasOpt(parsed.opts, 'list') || hasOpt(parsed.opts, 'l') + const foldersOnly = hasOpt(parsed.opts, 'folders') || hasOpt(parsed.opts, 'f') + const recordsOnly = hasOpt(parsed.opts, 'records') || hasOpt(parsed.opts, 'r') + const target = lsPath(parsed) + + const listOpts = { + detail, + showFolders: recordsOnly ? false : true, + showRecords: foldersOnly ? false : true, + } + + if (!target) { + const result = await v.listFolder!({ ...listOpts }) + return { code: 0, out: formatLs(result, detail), err: '' } + } + + if (!v.changeDirectory || !v.getCurrentFolderUid) { + return { code: 1, out: '', err: `${cmd}: host lacks navigation capabilities.\n` } + } + + const originalUid = v.getCurrentFolderUid() + let resolvedUid: string | null + try { + const cd = await v.changeDirectory(target) + resolvedUid = cd.folderUid + } catch (e) { + return { code: 1, out: '', err: host.formatError(`${cmd} ${target}`, e) } + } + try { + const result = await v.listFolder!({ folderUid: resolvedUid ?? null, ...listOpts }) + return { code: 0, out: formatLs(result, detail), err: '' } + } finally { + if (resolvedUid !== originalUid) { + try { + await v.changeDirectory(originalUid ?? '/') + } catch { + /* best-effort */ + } + } + } +} + +async function runCd(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { + const target = parsed.positional[0] + if (!target) return { code: 1, out: '', err: `${cmd}: missing folder path. Usage: ${cmd} \n` } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'changeDirectory', cmd) + if (cap) return cap + try { + const res = await v.changeDirectory!(target) + return { code: 0, out: `${res.name}\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`${cmd} ${target}`, e) } + } +} + +async function runTree(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'tree', cmd) + if (cap) return cap + await v.sync() + const folderPath = parsed.positional[0] + const out = await v.tree!(folderPath ? { folderPath } : {}) + return { code: 0, out: out.endsWith('\n') ? out : out + '\n', err: '' } +} + +async function runMkdir(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { + const target = parsed.positional[0] + if (!target) { + return { code: 1, out: '', err: `${cmd}: missing path. Usage: ${cmd} [-sf]\n` } + } + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + const cap = ensureCapability(v, 'mkdir', cmd) + if (cap) return cap + const cwd = v.getWorkingFolderDisplayName?.() ?? 'My Vault' + const shared = + hasOpt(parsed.opts, 'shared-folder') || + hasOpt(parsed.opts, 'sf') || + hasOpt(parsed.opts, 'shared') + try { + const res = await v.mkdir!(target, { sharedFolder: shared }) + if (!res.success) { + return { code: 1, out: '', err: `${cmd} [in ${cwd}]: ${res.message ?? 'failed'}\n` } + } + return { code: 0, out: `${res.folderUid}\t${target} (in ${cwd})\n`, err: '' } + } catch (e) { + return { code: 1, out: '', err: host.formatError(`${cmd} ${target} [in ${cwd}]`, e) } + } +} + +const lsHelp: CliCommandDefinition['help'] = { + title: 'ls — list folder contents (Keeper Commander)', + synopsis: 'usage: ls [-l] [-f] [-r] [pattern]', + description: ' Lists records and subfolders in the current folder, or in PATH if given.', + options: ` -l, --list Detailed list (flags, types). + -f, --folders Folders only. + -r, --records Records only. + --help, -h Show this help.`, + examples: ' ls\n ls "Marketing"\n ls -l', + seeAlso: ' cd, tree, get', +} + +export const lsCommand: CliCommandDefinition = { + name: 'ls', + order: 11, + description: 'List folder contents (current folder or PATH).', + usage: 'ls [PATH] [-l|--list] [-f|--folders] [-r|--records]', + flagOptions: ['-l', '--list', '-f', '--folders', '-r', '--records', '--detail'], + help: lsHelp, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(lsCommand), err: '' } + try { + return await runLs(host, parsed, 'ls') + } catch (e) { + return { code: 1, out: '', err: host.formatError('ls', e) } + } + }, +} + +export const cdCommand: CliCommandDefinition = { + name: 'cd', + order: 12, + description: 'Change current folder.', + usage: 'cd ', + help: { + title: 'cd — change current folder (Keeper Commander)', + synopsis: 'usage: cd ', + description: ' PATH is a slash-separated folder name/UID sequence, or `/` for vault root.', + examples: ' cd Marketing\n cd ..\n cd /', + seeAlso: ' ls, tree', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(cdCommand), err: '' } + try { + return await runCd(host, parsed, 'cd') + } catch (e) { + return { code: 1, out: '', err: host.formatError('cd', e) } + } + }, +} + +export const treeCommand: CliCommandDefinition = { + name: 'tree', + order: 13, + description: 'Display the folder structure.', + usage: 'tree [PATH]', + help: { + title: 'tree — folder structure (Keeper Commander)', + synopsis: 'usage: tree [folder]', + description: ' Renders an ASCII tree of folders (and records) from PATH or the vault root.', + seeAlso: ' ls, cd', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(treeCommand), err: '' } + try { + return await runTree(host, parsed, 'tree') + } catch (e) { + return { code: 1, out: '', err: host.formatError('tree', e) } + } + }, +} + +export const mkdirCommand: CliCommandDefinition = { + name: 'mkdir', + order: 14, + description: 'Create a folder.', + usage: 'mkdir [-sf|--shared-folder]', + flagOptions: ['-sf', '--shared-folder', '--shared'], + help: { + title: 'mkdir — create folder (Keeper Commander)', + synopsis: 'usage: mkdir [-sf]', + description: ' Creates a user folder under the current folder. -sf creates a shared folder.', + options: ' -sf, --shared-folder Create a shared folder.', + examples: ' mkdir Drafts\n mkdir TeamShare -sf', + seeAlso: ' cd, ls', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(mkdirCommand), err: '' } + try { + return await runMkdir(host, parsed, 'mkdir') + } catch (e) { + return { code: 1, out: '', err: host.formatError('mkdir', e) } + } + }, +} diff --git a/KeeperSdk/src/cli/commands/help.ts b/KeeperSdk/src/cli/commands/help.ts index 2ce80544..24f49db6 100644 --- a/KeeperSdk/src/cli/commands/help.ts +++ b/KeeperSdk/src/cli/commands/help.ts @@ -6,7 +6,8 @@ import { formatShortCommandSummary, getDetailedHelpPageForRegistry, } from '../help' -import { listCliCommands } from '../registry' +import { isAuthCliCommand, listCliCommandsForLoginState } from '../access' +import { getCliCommand } from '../registry' export const helpCommand: CliCommandDefinition = { name: 'help', @@ -16,36 +17,57 @@ export const helpCommand: CliCommandDefinition = { help: { title: 'help — show commands or short syntax for one command', synopsis: ' help [COMMAND]', - description: ` Without arguments, lists every built-in command with a one-line summary. - With COMMAND, prints the same overview line plus usage for that command. + description: ` Without arguments, lists commands for the current session. + When not logged in, only sign-in commands are listed (login, restore-session, …). + After login, lists vault commands as well. - For full documentation on each command, run: - COMMAND --help - COMMAND -h`, + With COMMAND, prints usage for that command (sign-in commands only when logged out).`, options: ' None. This command does not take GNU-style flags.', seeAlso: ' Each command’s --help output.', }, - async run(_host, parsed) { + async run(host, parsed) { if (wantsCliHelp(parsed)) { return { code: 0, out: formatDetailedHelpForCommand(helpCommand), err: '' } } if (parsed.opts.size > 0) { return { code: 1, out: '', err: 'help: unknown option (try `help --help`)\n' } } + const loggedIn = host.getVault().isLoggedIn + const visible = listCliCommandsForLoginState(loggedIn) const args = parsed.positional if (args.length === 0) { - return { code: 0, out: formatAllCommandsSummary(listCliCommands()), err: '' } + if (loggedIn) { + return { code: 0, out: formatAllCommandsSummary(visible), err: '' } + } + return { + code: 0, + out: formatAllCommandsSummary(visible, { + header: 'Not logged in — sign-in commands:\n\n', + footer: + '\nRun `login`, `restore-session`, or `register-device` to open the vault.\n' + + 'After login, run `help` again for vault commands (get, ls, cd, …).\n', + }), + err: '', + } } if (args.length > 1) { return { code: 1, out: '', err: 'Usage: help [command]\n' } } const name = args[0] - const long = getDetailedHelpPageForRegistry(listCliCommands(), name) + if (!loggedIn && !isAuthCliCommand(name)) { + return { + code: 1, + out: '', + err: + `help: "${name}" requires a logged-in session. ` + + 'Run `help` for sign-in commands (login, restore-session, register-device).\n', + } + } + const long = getDetailedHelpPageForRegistry(visible, name) if (long) { return { code: 0, out: long, err: '' } } - const commands = listCliCommands() - const def = commands.find((c) => c.name === name.toLowerCase()) + const def = getCliCommand(name) if (!def) { return { code: 1, out: '', err: `help: unknown command: ${name}\n` } } diff --git a/KeeperSdk/src/cli/commands/records.ts b/KeeperSdk/src/cli/commands/records.ts index 6eb8daa9..7201223b 100644 --- a/KeeperSdk/src/cli/commands/records.ts +++ b/KeeperSdk/src/cli/commands/records.ts @@ -1,10 +1,11 @@ -import { formatRecord, getRecordTitle } from '../../records/RecordUtils' +import { getRecordTitle } from '../../records/RecordUtils' import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' import { getOpt, hasOpt, wantsCliHelp } from '../parse' import { formatDetailedHelpForCommand } from '../help' import { recordUid } from '../utils' import { ensureCapability, ensureSession } from '../commandHelpers' import { formatTable } from '../table' +import { executeGet } from '../commander/getCore' const SUBCOMMANDS = ['list', 'get', 'find', 'share-info'] as const type Sub = (typeof SUBCOMMANDS)[number] @@ -26,21 +27,11 @@ async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise\n' } } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'findRecord', 'records get') - if (cap) return cap - await v.sync() - const record = v.findRecord!(target) - if (!record) { - return { code: 1, out: '', err: `records get: no record matching "${target}"\n` } - } - const detail = hasOpt(parsed.opts, 'detail') - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(record, null, 2) + '\n', err: '' } + const shifted: ParsedCli = { + positional: [target], + opts: parsed.opts, } - return { code: 0, out: formatRecord(record, detail) + '\n', err: '' } + return executeGet(host, shifted, 'records get') } async function runFind(host: KeeperCliHost, parsed: ParsedCli): Promise { @@ -106,11 +97,11 @@ export const recordsCommand: CliCommandDefinition = { description: 'Vault records (list, get, find, share-info).', usage: 'records list|get|find|share-info [args] [--detail] [--pattern] [--json] [--help|-h]', subcommands: [...SUBCOMMANDS], - flagOptions: ['--detail', '--pattern', '--json'], + flagOptions: ['--detail', '--pattern', '--json', '--format', '--unmask'], help: { title: 'records — search and inspect vault records', synopsis: ` records [list] - records get UID|TITLE [--detail] [--json] + records get UID|TITLE [--format {detail,json,password,fields}] [--unmask] [--detail] [--json] records find TEXT [--pattern TEXT] [--json] records share-info UID|TITLE`, description: ' list syncs and prints uid + title. get/find resolve by uid or title substring.', diff --git a/KeeperSdk/src/cli/commands/sync.ts b/KeeperSdk/src/cli/commands/sync.ts index f13284f3..7ce6ea4f 100644 --- a/KeeperSdk/src/cli/commands/sync.ts +++ b/KeeperSdk/src/cli/commands/sync.ts @@ -31,7 +31,7 @@ export async function runVaultSync(host: KeeperCliHost): Promise { export const syncCommand: CliCommandDefinition = { name: 'sync', order: 20, - aliases: ['syncdown'], + aliases: ['syncdown', 'sync-down', 'd'], description: 'Download / refresh vault data from Keeper (syncDown).', usage: 'sync [--help|-h]', help: { diff --git a/KeeperSdk/src/cli/dispatch.ts b/KeeperSdk/src/cli/dispatch.ts index 17b12133..b3be1427 100644 --- a/KeeperSdk/src/cli/dispatch.ts +++ b/KeeperSdk/src/cli/dispatch.ts @@ -3,8 +3,12 @@ import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' import { extractFromJsonFlagValue } from './jsonArg' import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' import { formatDetailedHelpForCommand } from './help' +import { isAuthCliCommand } from './access' import { getCliCommand } from './registry' +const NOT_LOGGED_IN_ERR = + 'Not logged in. Run `login`, `restore-session`, or `register-device` (see `help`).\n' + export async function dispatchKeeperCli( commandName: string, args: string[], @@ -15,6 +19,9 @@ export async function dispatchKeeperCli( if (!def) { return { code: 1, out: '', err: `Unknown command: ${commandName}\n` } } + if (!host.getVault().isLoggedIn && !isAuthCliCommand(def.name)) { + return { code: 1, out: '', err: NOT_LOGGED_IN_ERR } + } const parsed = preParsed ?? parseCliArgs(args) if (wantsCliHelp(parsed)) { return { code: 0, out: formatDetailedHelpForCommand(def), err: '' } diff --git a/KeeperSdk/src/cli/help.ts b/KeeperSdk/src/cli/help.ts index 6ebfeb26..40773a4f 100644 --- a/KeeperSdk/src/cli/help.ts +++ b/KeeperSdk/src/cli/help.ts @@ -55,14 +55,25 @@ export function getDetailedHelpPageForRegistry( return null } -export function formatAllCommandsSummary(commands: readonly CliCommandDefinition[]): string { +export type CommandsSummaryOptions = { + header?: string + footer?: string +} + +export function formatAllCommandsSummary( + commands: readonly CliCommandDefinition[], + options?: CommandsSummaryOptions +): string { const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name)) const w = Math.max(...sorted.map((c) => c.name.length), 8) - let out = 'Supported commands:\n\n' + let out = options?.header ?? 'Supported commands:\n\n' + if (!out.endsWith('\n\n')) { + out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n` + } for (const c of sorted) { out += ` ${c.name.padEnd(w)} ${c.description}\n` } - out += '\nRun ` --help` (or `-h`) for details on a specific command.\n' + out += options?.footer ?? '\nRun ` --help` (or `-h`) for details on a specific command.\n' return out } diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts index bf8c89f5..ee6a3241 100644 --- a/KeeperSdk/src/cli/index.ts +++ b/KeeperSdk/src/cli/index.ts @@ -1,16 +1,4 @@ -import { registerCliCommand } from './registry' -import { foldersCommand } from './commands/folders' -import { helpCommand } from './commands/help' -import { loginCommand } from './commands/login' -import { logoutCommand } from './commands/logout' -import { recordsCommand } from './commands/records' -import { registerDeviceCommand } from './commands/registerDevice' -import { restoreSessionCommand } from './commands/restoreSession' -import { sharedFoldersCommand } from './commands/sharedFolders' -import { syncCommand } from './commands/sync' -import { teamsCommand } from './commands/teams' -import { usersCommand } from './commands/users' -import { vaultCommand } from './commands/vault' +import { registerBuiltinCliCommands } from './builtinCommands' let registryInitialized = false @@ -18,18 +6,7 @@ let registryInitialized = false export function ensureKeeperCliRegistry(): void { if (registryInitialized) return registryInitialized = true - registerCliCommand(helpCommand) - registerCliCommand(loginCommand) - registerCliCommand(registerDeviceCommand) - registerCliCommand(restoreSessionCommand) - registerCliCommand(syncCommand) - registerCliCommand(vaultCommand) - registerCliCommand(recordsCommand) - registerCliCommand(foldersCommand) - registerCliCommand(sharedFoldersCommand) - registerCliCommand(teamsCommand) - registerCliCommand(usersCommand) - registerCliCommand(logoutCommand) + registerBuiltinCliCommands() } ensureKeeperCliRegistry() @@ -77,6 +54,14 @@ export { clearCliRegistry, } from './registry' +export { + AUTH_CLI_COMMAND_NAMES, + isAuthCliCommand, + filterCliCommandsForLoginState, + listCliCommandsForLoginState, + listCliCommandNamesForLoginState, +} from './access' + export { dispatchKeeperCli, dispatchCliLine } from './dispatch' export { KeeperCliParser, createKeeperCliParser } from './parser' @@ -102,6 +87,10 @@ export { helpCommand } from './commands/help' export { restoreSessionCommand } from './commands/restoreSession' export { syncCommand, runVaultSync } from './commands/sync' +export { getKeeperCliPromptPrefix } from './prompt' +export { BUILTIN_CLI_COMMANDS, registerBuiltinCliCommands } from './builtinCommands' +export { getCommand, executeGet, registerCommanderCliCommands } from './commander' + export { utf8ToBase64Url, recordUid } from './utils' export type { SessionRestoreInput } from '../auth/sessionRestore' diff --git a/KeeperSdk/src/cli/parser.ts b/KeeperSdk/src/cli/parser.ts index 6d341c95..7fa74311 100644 --- a/KeeperSdk/src/cli/parser.ts +++ b/KeeperSdk/src/cli/parser.ts @@ -2,8 +2,13 @@ import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from ' import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' import { extractFromJsonFlagValue } from './jsonArg' import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' +import { AUTH_CLI_COMMAND_NAMES, isAuthCliCommand } from './access' +import { BUILTIN_CLI_COMMANDS } from './builtinCommands' import { formatAllCommandsSummary, formatDetailedHelpForCommand, formatShortCommandSummary } from './help' +const NOT_LOGGED_IN_ERR = + 'Not logged in. Run `login`, `restore-session`, or `register-device` (see `help`).\n' + export type KeeperCliParserOptions = { prog?: string description?: string @@ -60,9 +65,20 @@ export class KeeperCliParser { return target ? this.commands.get(target) : undefined } - formatHelp(): string { + formatHelp(host?: KeeperCliHost): string { + const loggedIn = host?.getVault().isLoggedIn ?? true + const commands = loggedIn + ? this.list() + : this.list().filter((c) => AUTH_CLI_COMMAND_NAMES.has(c.name)) const header = this.description ? `${this.prog} — ${this.description}\n\n` : '' - const body = formatAllCommandsSummary(this.list()) + const body = loggedIn + ? formatAllCommandsSummary(commands) + : formatAllCommandsSummary(commands, { + header: 'Not logged in — sign-in commands:\n\n', + footer: + '\nRun `login`, `restore-session`, or `register-device` to open the vault.\n' + + 'After login, run `help` again for vault commands (get, ls, cd, …).\n', + }) const footer = this.epilog ? `\n${this.epilog}\n` : '' return header + body + footer } @@ -80,7 +96,7 @@ export class KeeperCliParser { async parse(line: string | readonly string[], host: KeeperCliHost): Promise { const { tokens, raw } = normalizeInput(line) if (tokens.length === 0) { - return ok(this.formatHelp()) + return ok(this.formatHelp(host)) } const first = tokens[0] @@ -88,7 +104,10 @@ export class KeeperCliParser { if (isHelpToken(first)) { const sub = rest[0] - if (!sub) return ok(this.formatHelp()) + if (!sub) return ok(this.formatHelp(host)) + if (!host.getVault().isLoggedIn && !isAuthCliCommand(sub)) { + return err(NOT_LOGGED_IN_ERR) + } const page = this.formatCommandHelp(sub) if (page) return ok(page) return err(`${this.prog}: unknown command: ${sub}\nTry: ${this.prog} --help\n`) @@ -99,6 +118,10 @@ export class KeeperCliParser { return err(`${this.prog}: unknown command: ${first}\nTry: ${this.prog} --help\n`) } + if (!host.getVault().isLoggedIn && !isAuthCliCommand(def.name)) { + return err(NOT_LOGGED_IN_ERR) + } + let parsed: ParsedCli if (def.name === 'restore-session') { const json = extractFromJsonFlagValue(raw, 'from-json', RESTORE_SESSION_TRAILING_OPTS) @@ -123,39 +146,7 @@ export function createKeeperCliParser(options: KeeperCliParserOptions = {}): Kee } function loadBuiltinsInto(parser: KeeperCliParser): void { - const { foldersCommand } = require('./commands/folders') as typeof import('./commands/folders') - const { helpCommand } = require('./commands/help') as typeof import('./commands/help') - const { loginCommand } = require('./commands/login') as typeof import('./commands/login') - const { logoutCommand } = require('./commands/logout') as typeof import('./commands/logout') - const { recordsCommand } = require('./commands/records') as typeof import('./commands/records') - const { - registerDeviceCommand, - } = require('./commands/registerDevice') as typeof import('./commands/registerDevice') - const { - restoreSessionCommand, - } = require('./commands/restoreSession') as typeof import('./commands/restoreSession') - const { syncCommand } = require('./commands/sync') as typeof import('./commands/sync') - const { vaultCommand } = require('./commands/vault') as typeof import('./commands/vault') - const { - sharedFoldersCommand, - } = require('./commands/sharedFolders') as typeof import('./commands/sharedFolders') - const { teamsCommand } = require('./commands/teams') as typeof import('./commands/teams') - const { usersCommand } = require('./commands/users') as typeof import('./commands/users') - - parser.addCommands([ - helpCommand, - loginCommand, - registerDeviceCommand, - restoreSessionCommand, - syncCommand, - vaultCommand, - recordsCommand, - foldersCommand, - sharedFoldersCommand, - teamsCommand, - usersCommand, - logoutCommand, - ]) + parser.addCommands(BUILTIN_CLI_COMMANDS) } function normalizeInput(line: string | readonly string[]): { tokens: string[]; raw: string } { diff --git a/KeeperSdk/src/cli/prompt.ts b/KeeperSdk/src/cli/prompt.ts new file mode 100644 index 00000000..0ac8ea99 --- /dev/null +++ b/KeeperSdk/src/cli/prompt.ts @@ -0,0 +1,12 @@ +import type { KeeperCliHost } from './types' + +const NOT_LOGGED_IN_PROMPT = 'Not logged in> ' +const PROMPT_MAX_LEN = 40 + +export function getKeeperCliPromptPrefix(host: KeeperCliHost): string { + const v = host.getVault() + if (!v.isLoggedIn) return NOT_LOGGED_IN_PROMPT + const name = v.getWorkingFolderDisplayName?.() ?? 'My Vault' + const label = name.length > PROMPT_MAX_LEN ? `...${name.slice(-37)}` : name + return `${label}> ` +} diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts index 67d3e990..eb9f76a5 100644 --- a/KeeperSdk/src/cli/types.ts +++ b/KeeperSdk/src/cli/types.ts @@ -69,6 +69,7 @@ export type KeeperCliHost = { envString(name: string): string | undefined formatError(context: string, err: unknown): string readTextFile?: (path: string) => Promise + getAccountUsername?: () => Promise } export type CliHelpDoc = { diff --git a/KeeperSdk/src/records/RecordUtils.ts b/KeeperSdk/src/records/RecordUtils.ts index 74630faa..15f3c744 100644 --- a/KeeperSdk/src/records/RecordUtils.ts +++ b/KeeperSdk/src/records/RecordUtils.ts @@ -250,7 +250,63 @@ function collectRecordWords(record: DRecord): string[] { return words } -export function formatRecord(record: DRecord, showDetails = false): string { +export type FormatRecordOptions = { + showDetails?: boolean + unmask?: boolean +} + +function resolveFormatRecordOptions(showDetailsOrOptions?: boolean | FormatRecordOptions): { + showDetails: boolean + unmask: boolean +} { + if (typeof showDetailsOrOptions === 'boolean') { + return { showDetails: showDetailsOrOptions, unmask: false } + } + return { + showDetails: showDetailsOrOptions?.showDetails ?? false, + unmask: showDetailsOrOptions?.unmask ?? false, + } +} + +function formatFieldValue(field: RecordField, unmask: boolean): string { + const isTotp = TOTP_FIELD_TYPES.has(field.type) + const isSensitive = field.type === FieldType.Password || isTotp || field.privacyScreen === true + if (!isSensitive || unmask) { + return field.value.map((v) => (typeof v === 'string' ? v : JSON.stringify(v))).join(', ') + } + return MASKED_VALUE +} + +export function formatRecordFields(record: DRecord, unmask: boolean): { name: string; value: unknown }[] { + const summary = getRecordSummary(record) + const fields: { name: string; value: unknown }[] = [ + { name: 'title', value: getRecordTitle(record) }, + { name: 'record_uid', value: record.uid }, + { name: 'version', value: record.version }, + { name: 'record_type', value: getRecordType(record) }, + ] + if (summary.login) fields.push({ name: 'login', value: summary.login }) + if (summary.password) { + fields.push({ + name: 'password', + value: unmask ? summary.password : MASKED_VALUE, + }) + } + if (summary.url) fields.push({ name: 'login_url', value: summary.url }) + for (const field of summary.fields) { + if (field.type === FieldType.Login || field.type === FieldType.Url) continue + const label = TOTP_FIELD_TYPES.has(field.type) + ? 'TOTP URL' + : (field.label || field.type).replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + fields.push({ name: label, value: formatFieldValue(field, unmask) }) + } + const notes = record.version <= RecordVersion.Legacy ? record.data?.notes : undefined + if (notes) fields.push({ name: 'Notes', value: notes }) + return fields +} + +export function formatRecord(record: DRecord, showDetailsOrOptions?: boolean | FormatRecordOptions): string { + const { showDetails, unmask } = resolveFormatRecordOptions(showDetailsOrOptions) const summary = getRecordSummary(record) const lines: string[] = [ RECORD_SEPARATOR, @@ -261,17 +317,23 @@ export function formatRecord(record: DRecord, showDetails = false): string { if (summary.login) lines.push(`Username: ${summary.login}`) if (summary.url) lines.push(`URL: ${summary.url}`) + if (summary.password) { + lines.push(`Password: ${unmask ? summary.password : MASKED_VALUE}`) + } if (showDetails) { for (const field of summary.fields) { if (field.type === FieldType.Login || field.type === FieldType.Url) continue + if (field.type === FieldType.Password) continue const isTotp = TOTP_FIELD_TYPES.has(field.type) - const isSensitive = field.type === FieldType.Password || isTotp const label = isTotp ? 'TOTP URL' : field.label || field.type - lines.push(`${label}: ${isSensitive ? MASKED_VALUE : field.value.join(', ')}`) + lines.push(`${label}: ${formatFieldValue(field, unmask)}`) } const totpUrl = getRecordTotpUrl(record) + if (unmask && totpUrl) { + lines.push(`TOTP URL: ${totpUrl}`) + } const code = totpUrl ? getTotpCode(totpUrl) : null if (code) { lines.push(`Two Factor Code: ${code.code} valid for ${code.secondsRemaining} sec`) diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index a8d52b00..8248dd0f 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -343,6 +343,10 @@ export class KeeperVault { this.log.info(`Session restored for ${params.username}`) } + public async getAccountUsername(): Promise { + return this.sessionManager.getLastUsername() + } + public async resumeSession(): Promise { const username = await this.sessionManager.getLastUsername() if (!username) { diff --git a/examples/sdk_example/src/utils/shellCliHost.ts b/examples/sdk_example/src/utils/shellCliHost.ts index e4fceb5d..35b35680 100644 --- a/examples/sdk_example/src/utils/shellCliHost.ts +++ b/examples/sdk_example/src/utils/shellCliHost.ts @@ -115,4 +115,5 @@ export const exampleShellCliHost: KeeperCliHost = { envString, formatError: formatKeeperClientError, readTextFile, + getAccountUsername: () => getVault().getAccountUsername(), } diff --git a/shellcomponent/.gitignore b/shellcomponent/.gitignore index a40b3a47..c643d230 100644 --- a/shellcomponent/.gitignore +++ b/shellcomponent/.gitignore @@ -1,4 +1,5 @@ .env.local +**/.vite **/node_modules **/dist **/build diff --git a/shellcomponent/index.html b/shellcomponent/index.html index 59f8c953..332ee899 100644 --- a/shellcomponent/index.html +++ b/shellcomponent/index.html @@ -26,7 +26,6 @@

Web console (dev)

- diff --git a/shellcomponent/keeper-shell.d.ts b/shellcomponent/keeper-shell.d.ts index 4afab90f..fc807815 100644 --- a/shellcomponent/keeper-shell.d.ts +++ b/shellcomponent/keeper-shell.d.ts @@ -28,7 +28,10 @@ export class WebConsoleElement extends KeeperShell {} export type WebConsole = WebConsoleElement; export function dispatchCliLine(line: string): Promise; -export function completeCliLine(line: string): { +export function completeCliLine( + line: string, + options?: { loggedIn?: boolean; host?: import("@keeper-security/keeper-sdk-javascript").KeeperCliHost } +): { base: string; candidates: string[]; }; diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts index 08bad5b0..7fed08cf 100644 --- a/shellcomponent/src/KeeperShell.ts +++ b/shellcomponent/src/KeeperShell.ts @@ -3,7 +3,9 @@ import { FitAddon } from "@xterm/addon-fit"; import xtermCss from "@xterm/xterm/css/xterm.css?inline"; import { completeCliLine } from "./cli/cliComplete.js"; +import { getKeeperCliPromptPrefix } from "@keeper-security/keeper-sdk-javascript"; import { dispatchCliLine } from "./cli/cliDispatch.js"; +import { shellKeeperCliHost } from "./cli/keeperCliHost.js"; import { setShellCliContext } from "./cli/cliContext.js"; import { loginWithCredentials, resetShellVault } from "./cli/keeperCommands.js"; @@ -58,7 +60,8 @@ function longestCommonPrefix(values: string[]): string { return pref; } -const PROMPT_PREFIX = "$ "; +/** Fallback when SDK prompt helper is unavailable. */ +const FALLBACK_PROMPT_PREFIX = "$ "; /** Screen rows used by prompt + input when the line wraps (cols from xterm). */ function promptDisplayRows(displayChars: number, cols: number): number { @@ -659,9 +662,17 @@ export class KeeperShell extends HTMLElement { if (this.hasAttribute(ATTR_MASK_INPUT)) maskSensitive = true; }; + const promptPrefix = (): string => { + try { + return getKeeperCliPromptPrefix(shellKeeperCliHost); + } catch { + return FALLBACK_PROMPT_PREFIX; + } + }; + const writeFreshPrompt = (): void => { promptRows = 1; - term.write(PROMPT_PREFIX); + term.write(promptPrefix()); afterNewPrompt(); }; @@ -677,9 +688,10 @@ export class KeeperShell extends HTMLElement { if (cursorPos < 0) cursorPos = 0; if (cursorPos > line.length) cursorPos = line.length; const visible = maskDisplayActive() ? "*".repeat(line.length) : line; - const displayChars = PROMPT_PREFIX.length + visible.length; + const prefix = promptPrefix(); + const displayChars = prefix.length + visible.length; clearPromptRows(); - term.write(`${PROMPT_PREFIX}${visible}`); + term.write(`${prefix}${visible}`); promptRows = promptDisplayRows(displayChars, term.cols); const back = line.length - cursorPos; if (back > 0) term.write(`\x1b[${back}D`); @@ -744,7 +756,7 @@ export class KeeperShell extends HTMLElement { this._completing = true; try { bumpEditing(); - const data = completeCliLine(this._lineBuf) as CompleteResponse; + const data = completeCliLine(this._lineBuf, { host: shellKeeperCliHost }) as CompleteResponse; const base = typeof data.base === "string" ? data.base : ""; const raw = data.candidates; const candidates = Array.isArray(raw) @@ -943,9 +955,15 @@ export class KeeperShell extends HTMLElement { }); term.writeln("\x1b[1mWeb console\x1b[0m — commands run in this browser (Keeper SDK + CLI)."); - term.writeln( - "Type `help` — records, folders, shared-folders, teams, users, vault summary (see each COMMAND --help)." - ); + if (shellKeeperCliHost.getVault().isLoggedIn) { + term.writeln( + "Keeper Commander-style CLI: get, ls, cd, tree, search, sync-down, help (see COMMAND --help)." + ); + } else { + term.writeln( + "Not logged in — use login, restore-session, or register-device. Type help for sign-in commands." + ); + } term.writeln("Tab completion and masked password entry are handled locally."); term.writeln("Optional: `keeper-host` attribute (or VITE_KEEPER_HOST) for vault region."); term.writeln("Up / Down — history; Left / Right — move cursor; Delete — forward delete."); diff --git a/shellcomponent/src/cli/cliComplete.ts b/shellcomponent/src/cli/cliComplete.ts index 77bed47e..1595c994 100644 --- a/shellcomponent/src/cli/cliComplete.ts +++ b/shellcomponent/src/cli/cliComplete.ts @@ -1,7 +1,11 @@ /** * Tab-completion for keeper-shell (SDK command registry). */ -import { getCliCommand, listCliCommandNames } from "@keeper-security/keeper-sdk-javascript"; +import { + getCliCommand, + listCliCommandNamesForLoginState, + type KeeperCliHost, +} from "@keeper-security/keeper-sdk-javascript"; const HELP_FLAGS = ["--help", "-h"] as const; @@ -16,7 +20,13 @@ export type CliCompleteResult = { candidates: string[]; }; -export function completeCliLine(line: string): CliCompleteResult { +export type CliCompleteOptions = { + loggedIn?: boolean + host?: KeeperCliHost +} + +export function completeCliLine(line: string, options?: CliCompleteOptions): CliCompleteResult { + const loggedIn = options?.host?.getVault().isLoggedIn ?? options?.loggedIn ?? true const completesNewWord = /\s$/.test(line); const segments = line.match(/\S+/g) ?? []; @@ -41,7 +51,7 @@ export function completeCliLine(line: string): CliCompleteResult { partialLen > 0 ? line.slice(0, line.length - partialLen) : line; if (words.length === 0) { - const top = listCliCommandNames(); + const top = [...listCliCommandNamesForLoginState(loggedIn)]; const hits = top.filter((c) => c.startsWith(stubLc)); return { base: baseFor(stub.length), candidates: hits }; } diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts index 6ce0ba19..31749689 100644 --- a/shellcomponent/src/cli/keeperCliHost.ts +++ b/shellcomponent/src/cli/keeperCliHost.ts @@ -174,4 +174,5 @@ export const shellKeeperCliHost: KeeperCliHost = { envString, formatError: formatKeeperClientError, readTextFile, + getAccountUsername: () => getVault().getAccountUsername(), }; From f4a64f98079a9718291a244c7b89ce2499a3e0fa Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 8 Jun 2026 18:21:27 +0530 Subject: [PATCH 15/23] refactor to remove old implementations --- KeeperSdk/src/api.ts | 2 +- KeeperSdk/src/cli/builtinCommands.ts | 18 +- KeeperSdk/src/cli/commander/index.ts | 26 +- KeeperSdk/src/cli/commander/misc.ts | 39 +++ KeeperSdk/src/cli/commands/folders.ts | 299 ------------------ KeeperSdk/src/cli/commands/records.ts | 149 --------- KeeperSdk/src/cli/commands/sharedFolders.ts | 66 ---- KeeperSdk/src/cli/commands/sync.ts | 2 +- KeeperSdk/src/cli/commands/teams.ts | 99 ------ KeeperSdk/src/cli/commands/users.ts | 2 +- KeeperSdk/src/cli/commands/vault.ts | 2 +- KeeperSdk/src/cli/index.ts | 20 +- KeeperSdk/src/utils/constants.ts | 4 - .../sdk_example/src/utils/shellCliRestore.ts | 8 +- shellcomponent/src/KeeperShell.ts | 45 ++- shellcomponent/src/cli/shellBanner.ts | 266 ++++++++++++++++ 16 files changed, 358 insertions(+), 689 deletions(-) delete mode 100644 KeeperSdk/src/cli/commands/folders.ts delete mode 100644 KeeperSdk/src/cli/commands/records.ts delete mode 100644 KeeperSdk/src/cli/commands/sharedFolders.ts delete mode 100644 KeeperSdk/src/cli/commands/teams.ts create mode 100644 shellcomponent/src/cli/shellBanner.ts diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts index d8e1583b..def13c4e 100644 --- a/KeeperSdk/src/api.ts +++ b/KeeperSdk/src/api.ts @@ -209,7 +209,7 @@ export { getKeeperCliPromptPrefix, BUILTIN_CLI_COMMANDS, registerBuiltinCliCommands, - registerCommanderCliCommands, + listCommand, } from './cli' export type { KeeperCliParserOptions } from './cli' export type { diff --git a/KeeperSdk/src/cli/builtinCommands.ts b/KeeperSdk/src/cli/builtinCommands.ts index 88cb2917..aed2457d 100644 --- a/KeeperSdk/src/cli/builtinCommands.ts +++ b/KeeperSdk/src/cli/builtinCommands.ts @@ -1,21 +1,24 @@ import type { CliCommandDefinition } from './types' import { registerCliCommand } from './registry' -import { foldersCommand } from './commands/folders' import { helpCommand } from './commands/help' import { loginCommand } from './commands/login' import { logoutCommand } from './commands/logout' -import { recordsCommand } from './commands/records' import { registerDeviceCommand } from './commands/registerDevice' import { restoreSessionCommand } from './commands/restoreSession' -import { sharedFoldersCommand } from './commands/sharedFolders' import { syncCommand } from './commands/sync' -import { teamsCommand } from './commands/teams' import { usersCommand } from './commands/users' import { vaultCommand } from './commands/vault' import { getCommand } from './commander/get' import { cdCommand, lsCommand, mkdirCommand, treeCommand } from './commander/nav' -import { listSfCommand, listTeamCommand, searchCommand, whoamiCommand } from './commander/misc' +import { + listCommand, + listSfCommand, + listTeamCommand, + searchCommand, + whoamiCommand, +} from './commander/misc' +/** Built-in CLI commands (Keeper Commander-style vault shell). */ export const BUILTIN_CLI_COMMANDS: readonly CliCommandDefinition[] = [ helpCommand, loginCommand, @@ -24,6 +27,7 @@ export const BUILTIN_CLI_COMMANDS: readonly CliCommandDefinition[] = [ syncCommand, vaultCommand, getCommand, + listCommand, lsCommand, cdCommand, treeCommand, @@ -32,10 +36,6 @@ export const BUILTIN_CLI_COMMANDS: readonly CliCommandDefinition[] = [ listSfCommand, listTeamCommand, whoamiCommand, - recordsCommand, - foldersCommand, - sharedFoldersCommand, - teamsCommand, usersCommand, logoutCommand, ] diff --git a/KeeperSdk/src/cli/commander/index.ts b/KeeperSdk/src/cli/commander/index.ts index 6461335e..fb25cce5 100644 --- a/KeeperSdk/src/cli/commander/index.ts +++ b/KeeperSdk/src/cli/commander/index.ts @@ -1,28 +1,4 @@ -import { registerCliCommand } from '../registry' -import { getCommand } from './get' -import { cdCommand, lsCommand, mkdirCommand, treeCommand } from './nav' -import { listSfCommand, listTeamCommand, searchCommand, whoamiCommand } from './misc' - export { getCommand } from './get' export { executeGet } from './getCore' export { lsCommand, cdCommand, treeCommand, mkdirCommand } from './nav' -export { searchCommand, listSfCommand, listTeamCommand, whoamiCommand } from './misc' - -const COMMANDER_COMMANDS = [ - getCommand, - lsCommand, - cdCommand, - treeCommand, - mkdirCommand, - searchCommand, - listSfCommand, - listTeamCommand, - whoamiCommand, -] - -/** Register top-level Keeper Commander-style vault commands (JS SDK under the hood). */ -export function registerCommanderCliCommands(): void { - for (const def of COMMANDER_COMMANDS) { - registerCliCommand(def) - } -} +export { listCommand, searchCommand, listSfCommand, listTeamCommand, whoamiCommand } from './misc' diff --git a/KeeperSdk/src/cli/commander/misc.ts b/KeeperSdk/src/cli/commander/misc.ts index 3f6b1dfd..5162773a 100644 --- a/KeeperSdk/src/cli/commander/misc.ts +++ b/KeeperSdk/src/cli/commander/misc.ts @@ -8,6 +8,22 @@ import { recordUid } from '../utils' import { formatSharedFoldersTable, renderSharedFoldersAsciiTable } from '../../sharedFolders/listSharedFolders' import { formatTeamsTable, renderTeamsAsciiTable } from '../../teams/listTeams' +async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { + const r = await ensureSession(host) + if (r) return r + const v = host.getVault() + await v.sync() + const records = v.getRecords() + const rows = records.map((rec) => [recordUid(rec), getRecordTitle(rec)]) + if (hasOpt(parsed.opts, 'json')) { + return { code: 0, out: JSON.stringify(records, null, 2) + '\n', err: '' } + } + if (rows.length === 0) { + return { code: 0, out: '(no records)\n', err: '' } + } + return { code: 0, out: formatTable(['record_uid', 'title'], rows), err: '' } +} + async function runSearch(host: KeeperCliHost, parsed: ParsedCli): Promise { const pattern = parsed.positional.join(' ') || getOpt(parsed.opts, 'pattern') if (!pattern?.trim()) { @@ -93,6 +109,29 @@ async function runWhoami(host: KeeperCliHost): Promise { return { code: 0, out: lines.join('\n') + '\n', err: '' } } +export const listCommand: CliCommandDefinition = { + name: 'list', + order: 14, + aliases: ['l'], + description: 'List all vault records (uid and title).', + usage: 'list [--json]', + flagOptions: ['--json'], + help: { + title: 'list — all records (Keeper Commander)', + synopsis: 'usage: list [--json]', + description: ' Syncs and prints every record uid + title in the vault.', + seeAlso: ' get, search, ls', + }, + async run(host, parsed) { + if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(listCommand), err: '' } + try { + return await runList(host, parsed) + } catch (e) { + return { code: 1, out: '', err: host.formatError('list', e) } + } + }, +} + export const searchCommand: CliCommandDefinition = { name: 'search', order: 15, diff --git a/KeeperSdk/src/cli/commands/folders.ts b/KeeperSdk/src/cli/commands/folders.ts deleted file mode 100644 index cd840c50..00000000 --- a/KeeperSdk/src/cli/commands/folders.ts +++ /dev/null @@ -1,299 +0,0 @@ -import type { DSharedFolder } from '@keeper-security/keeperapi' -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { formatTable } from '../table' - -const SUBCOMMANDS = ['list', 'tree', 'ls', 'pwd', 'cd', 'mkdir', 'rename', 'rmdir', 'get'] as const -type Sub = (typeof SUBCOMMANDS)[number] - -async function runListShared(host: KeeperCliHost): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - await v.sync() - const folders = v.getSharedFolders() - if (folders.length === 0) return { code: 0, out: '(no shared folders)\n', err: '' } - const rows = folders.map((f: DSharedFolder) => [f.uid ?? '(unknown uid)', f.name ?? '(unnamed)']) - return { code: 0, out: formatTable(['shared_folder_uid', 'name'], rows), err: '' } -} - -async function runTree(host: KeeperCliHost): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'tree', 'tree') - if (cap) return cap - await v.sync() - const out = await v.tree!() - return { code: 0, out: out.endsWith('\n') ? out : out + '\n', err: '' } -} - -async function runLs(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listFolder', 'ls') - if (cap) return cap - await v.sync() - const target = parsed.positional[1] - const detail = hasOpt(parsed.opts, 'detail') - - if (!target) { - const result = await v.listFolder!({ detail }) - return { code: 0, out: formatLs(result, detail), err: '' } - } - - if (!v.changeDirectory || !v.getCurrentFolderUid) { - return { code: 1, out: '', err: 'folders ls : host lacks navigation capabilities.\n' } - } - - // Snapshot cwd, resolve via cd, list, then restore. ls is read-only; - // path resolution should never leave the session somewhere new. - const originalUid = v.getCurrentFolderUid() - let resolvedUid: string | null - try { - const cd = await v.changeDirectory(target) - resolvedUid = cd.folderUid - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders ls ${target}`, e) } - } - try { - const result = await v.listFolder!({ folderUid: resolvedUid ?? null, detail }) - return { code: 0, out: formatLs(result, detail), err: '' } - } finally { - if (resolvedUid !== originalUid) { - try { - await v.changeDirectory(originalUid ?? '/') - } catch { - // best-effort restore; user can `folders cd /` if needed - } - } - } -} - -function formatLs( - result: { - detail: boolean - folders: Array<{ uid: string; name: string }> - records: Array<{ uid: string; name: string; type?: string }> - }, - detail: boolean -): string { - if (result.folders.length + result.records.length === 0) return '(empty)\n' - - const headers = detail ? ['flags', 'uid', 'name', 'type'] : ['kind', 'uid', 'name'] - const rows: string[][] = [] - for (const f of result.folders) { - const flags = ((f as { flags?: string }).flags ?? '').trim() - rows.push(detail ? [flags || 'f---', f.uid, f.name, ''] : ['dir', f.uid, f.name]) - } - for (const r of result.records) { - const flags = ((r as { flags?: string }).flags ?? '').trim() - const type = r.type ?? '' - rows.push(detail ? [flags || 'r---', r.uid, r.name, type] : ['rec', r.uid, r.name]) - } - return formatTable(headers, rows) -} - -async function runPwd(host: KeeperCliHost): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'getWorkingFolderDisplayName', 'pwd') - if (cap) return cap - return { code: 0, out: `${v.getWorkingFolderDisplayName!()}\n`, err: '' } -} - -async function runCd(host: KeeperCliHost, parsed: ParsedCli): Promise { - const target = parsed.positional[1] - if (!target) return { code: 1, out: '', err: 'folders cd: missing path. Usage: folders cd \n' } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'changeDirectory', 'cd') - if (cap) return cap - try { - const res = await v.changeDirectory!(target) - return { code: 0, out: `${res.name}\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders cd ${target}`, e) } - } -} - -async function runMkdir(host: KeeperCliHost, parsed: ParsedCli): Promise { - const target = parsed.positional[1] - if (!target) { - return { - code: 1, - out: '', - err: 'folders mkdir: missing path. Usage: folders mkdir [--shared]\n', - } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'mkdir', 'mkdir') - if (cap) return cap - const cwd = v.getWorkingFolderDisplayName?.() ?? '(root)' - try { - const res = await v.mkdir!(target, { sharedFolder: hasOpt(parsed.opts, 'shared') }) - if (!res.success) { - return { code: 1, out: '', err: `folders mkdir [in ${cwd}]: ${res.message ?? 'failed'}\n` } - } - return { code: 0, out: `${res.folderUid}\t${target} (in ${cwd})\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders mkdir ${target} [in ${cwd}]`, e) } - } -} - -async function runRename(host: KeeperCliHost, parsed: ParsedCli): Promise { - const path = parsed.positional[1] - const newName = parsed.positional[2] - if (!path || !newName) { - return { - code: 1, - out: '', - err: 'folders rename: usage: folders rename \n', - } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'renameFolder', 'folders rename') - if (cap) return cap - try { - const res = await v.renameFolder!(path, newName) - if (!res.success) { - return { code: 1, out: '', err: `folders rename: ${res.message ?? 'failed'}\n` } - } - return { code: 0, out: `renamed ${path} → ${newName}\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders rename ${path}`, e) } - } -} - -async function runRmdir(host: KeeperCliHost, parsed: ParsedCli): Promise { - const pattern = parsed.positional[1] - if (!pattern) { - return { code: 1, out: '', err: 'folders rmdir: missing path. Usage: folders rmdir \n' } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'rmdir', 'folders rmdir') - if (cap) return cap - try { - const res = await v.rmdir!([pattern]) - if (!res.success) { - return { code: 1, out: '', err: `folders rmdir: ${res.message ?? 'failed'}\n` } - } - return { code: 0, out: `removed ${pattern}\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders rmdir ${pattern}`, e) } - } -} - -async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise { - const target = parsed.positional[1] - if (!target) return { code: 1, out: '', err: 'folders get: missing UID or name. Usage: folders get \n' } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'getFolder', 'get') - if (cap) return cap - try { - const res = await v.getFolder!(target, { format: 'json' }) - const json = (res as { json?: Record }).json ?? res - return { code: 0, out: JSON.stringify(json, null, 2) + '\n', err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders get ${target}`, e) } - } -} - -export const foldersCommand: CliCommandDefinition = { - name: 'folders', - order: 31, - description: 'List/navigate vault folders (list, tree, ls, pwd, cd, mkdir, rename, rmdir, get).', - usage: 'folders [list|tree|ls|pwd|cd|mkdir|rename|rmdir|get] [args] [--help|-h]', - subcommands: [...SUBCOMMANDS], - flagOptions: ['--shared', '--detail'], - help: { - title: 'folders — navigate and inspect vault folders', - synopsis: ` folders [list] Top-level shared folders (uid + name) - folders tree Render the full folder tree - folders ls [PATH] [--detail] Contents of a folder (default: current) - folders pwd Print current working folder display name - folders cd PATH Change current folder - folders mkdir PATH [--shared] Create a user (or shared) folder - folders rename PATH NEW_NAME Rename a folder - folders rmdir PATH Delete a folder - folders get UID|NAME Print folder details as JSON`, - description: ` Folder navigation maintained per-shell. The current folder affects - subsequent ls, mkdir, and other folder-relative operations. - - PATH is a slash-separated sequence of folder names or UIDs (e.g. - "Marketing/Q3" or "AAAA-bbbb-uid"). "/" is the vault root. - - list (default) calls sync() then enumerates shared folders only — - useful for a quick top-level view. Use tree or ls for full contents.`, - arguments: ` list (default) Print shared_folder_uid + name for top-level shared folders. - tree Render the full folder tree (folders + records) as ASCII. - ls PATH List immediate children (folders + records). Default: current folder. - pwd Print the current working folder. - cd PATH Change current folder. "/" returns to root. - mkdir PATH [--shared] Create a folder. --shared makes it a shared folder. - rename PATH NEW_NAME Rename a folder under cwd or by path. - rmdir PATH Delete a folder (by path or name). - get UID|NAME Print folder metadata as JSON.`, - options: ` --detail ls only: include flags, record types, ownership. - --shared mkdir only: create a shared folder instead of a user folder. - --help, -h Show this help.`, - examples: ` folders # shared folders quick list - folders tree # full tree - folders ls Marketing # contents of Marketing - folders cd Marketing/Q3 # navigate - folders mkdir Drafts # user folder under cwd - folders mkdir Public --shared # new shared folder under cwd - folders get aaaa-bbbb-uid # folder details as JSON`, - seeAlso: ' records list, sync, login, restore-session', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(foldersCommand), err: '' } - } - const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub - if (!SUBCOMMANDS.includes(sub)) { - return { - code: 1, - out: '', - err: `folders: unknown subcommand "${parsed.positional[0]}". Try: folders --help\n`, - } - } - try { - switch (sub) { - case 'list': - return await runListShared(host) - case 'tree': - return await runTree(host) - case 'ls': - return await runLs(host, parsed) - case 'pwd': - return await runPwd(host) - case 'cd': - return await runCd(host, parsed) - case 'mkdir': - return await runMkdir(host, parsed) - case 'rename': - return await runRename(host, parsed) - case 'rmdir': - return await runRmdir(host, parsed) - case 'get': - return await runGet(host, parsed) - } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`folders ${sub}`, e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/records.ts b/KeeperSdk/src/cli/commands/records.ts deleted file mode 100644 index 7201223b..00000000 --- a/KeeperSdk/src/cli/commands/records.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { getRecordTitle } from '../../records/RecordUtils' -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { recordUid } from '../utils' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { formatTable } from '../table' -import { executeGet } from '../commander/getCore' - -const SUBCOMMANDS = ['list', 'get', 'find', 'share-info'] as const -type Sub = (typeof SUBCOMMANDS)[number] - -async function runList(host: KeeperCliHost): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - await v.sync() - const records = v.getRecords() - const rows = records.map((rec) => [recordUid(rec), getRecordTitle(rec)]) - const header = 'record_uid\ttitle\n' - const body = rows.length ? rows.join('\n') + '\n' : '(no records)\n' - return { code: 0, out: header + body, err: '' } -} - -async function runGet(host: KeeperCliHost, parsed: ParsedCli): Promise { - const target = parsed.positional[1] - if (!target) { - return { code: 1, out: '', err: 'records get: missing UID or title. Usage: records get \n' } - } - const shifted: ParsedCli = { - positional: [target], - opts: parsed.opts, - } - return executeGet(host, shifted, 'records get') -} - -async function runFind(host: KeeperCliHost, parsed: ParsedCli): Promise { - const criteria = parsed.positional[1] ?? getOpt(parsed.opts, 'pattern') - if (!criteria) { - return { - code: 1, - out: '', - err: 'records find: missing search text. Usage: records find or records find --pattern \n', - } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'findRecords', 'records find') - if (cap) return cap - await v.sync() - const matches = v.findRecords!(criteria) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(matches, null, 2) + '\n', err: '' } - } - if (matches.length === 0) { - return { code: 0, out: `(no records matched "${criteria}")\n`, err: '' } - } - const rows = matches.map((rec) => [recordUid(rec), getRecordTitle(rec)]) - return { code: 0, out: formatTable(['record_uid', 'title'], rows), err: '' } -} - -async function runShareInfo(host: KeeperCliHost, parsed: ParsedCli): Promise { - const target = parsed.positional[1] - if (!target) { - return { - code: 1, - out: '', - err: 'records share-info: missing UID or title. Usage: records share-info \n', - } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - if (!v.findRecord || !v.getRecordShareInfo) { - return { - code: 1, - out: '', - err: 'records share-info: host lacks findRecord or getRecordShareInfo.\n', - } - } - await v.sync() - const record = v.findRecord(target) - if (!record?.uid) { - return { code: 1, out: '', err: `records share-info: no record matching "${target}"\n` } - } - const info = await v.getRecordShareInfo(record.uid) - if (!info) { - return { code: 0, out: '(no share information)\n', err: '' } - } - return { code: 0, out: JSON.stringify(info, null, 2) + '\n', err: '' } -} - -export const recordsCommand: CliCommandDefinition = { - name: 'records', - order: 30, - description: 'Vault records (list, get, find, share-info).', - usage: 'records list|get|find|share-info [args] [--detail] [--pattern] [--json] [--help|-h]', - subcommands: [...SUBCOMMANDS], - flagOptions: ['--detail', '--pattern', '--json', '--format', '--unmask'], - help: { - title: 'records — search and inspect vault records', - synopsis: ` records [list] - records get UID|TITLE [--format {detail,json,password,fields}] [--unmask] [--detail] [--json] - records find TEXT [--pattern TEXT] [--json] - records share-info UID|TITLE`, - description: ' list syncs and prints uid + title. get/find resolve by uid or title substring.', - arguments: ` list (default) Table of all records. - get One record (formatted text or --json). - find Search records by uid/title tokens. - share-info JSON share permissions for a record.`, - options: ` --detail get only: include extra fields in formatted output. - --pattern find only: same as positional search text. - --json JSON output (get, find, share-info). - --help, -h Show this help.`, - examples: ` records list - records get "My Login" --detail - records find password - records share-info abc123uid`, - seeAlso: ' folders ls, vault summary, sync', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(recordsCommand), err: '' } - } - const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub - if (!SUBCOMMANDS.includes(sub)) { - return { - code: 1, - out: '', - err: `records: unknown subcommand "${parsed.positional[0]}". Try: records --help\n`, - } - } - try { - switch (sub) { - case 'list': - return await runList(host) - case 'get': - return await runGet(host, parsed) - case 'find': - return await runFind(host, parsed) - case 'share-info': - return await runShareInfo(host, parsed) - } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`records ${sub}`, e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/sharedFolders.ts b/KeeperSdk/src/cli/commands/sharedFolders.ts deleted file mode 100644 index 77324c3e..00000000 --- a/KeeperSdk/src/cli/commands/sharedFolders.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { formatSharedFoldersTable, renderSharedFoldersAsciiTable } from '../../sharedFolders/listSharedFolders' - -async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listSharedFolders', 'shared-folders list') - if (cap) return cap - await v.sync!() - const pattern = getOpt(parsed.opts, 'pattern') ?? null - const verbose = hasOpt(parsed.opts, 'verbose') - const rows = v.listSharedFolders!({ pattern, verbose, includeDetails: verbose }) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } - } - if (rows.length === 0) { - return { - code: 0, - out: pattern ? `(no shared folders matched "${pattern}")\n` : '(no shared folders)\n', - err: '', - } - } - const table = formatSharedFoldersTable(rows, { verbose }) - return { code: 0, out: renderSharedFoldersAsciiTable(table) + '\n', err: '' } -} - -export const sharedFoldersCommand: CliCommandDefinition = { - name: 'shared-folders', - order: 32, - description: 'List shared folders (with optional counts).', - usage: 'shared-folders [list] [--pattern P] [--verbose] [--json] [--help|-h]', - subcommands: ['list'], - flagOptions: ['--pattern', '--verbose', '--json'], - help: { - title: 'shared-folders — list shared folders in the vault', - synopsis: ' shared-folders [list] [--pattern P] [--verbose] [--json]', - description: - ' Unlike `folders list` (uid + name only), this command can include team/user/record counts with --verbose.', - arguments: ' list (default) List shared folders.', - options: ` --pattern Filter by name or uid substring. - --verbose Include team/user/record counts and default permissions. - --json Emit JSON. - --help, -h Show this help.`, - examples: ` shared-folders - shared-folders list --pattern marketing --verbose`, - seeAlso: ' folders list, folders ls, sync', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(sharedFoldersCommand), err: '' } - } - const sub = parsed.positional[0]?.toLowerCase() - if (sub && sub !== 'list') { - return { code: 1, out: '', err: 'Usage: shared-folders [list]\n' } - } - try { - return await runList(host, parsed) - } catch (e) { - return { code: 1, out: '', err: host.formatError('shared-folders', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/sync.ts b/KeeperSdk/src/cli/commands/sync.ts index 7ce6ea4f..72d71f67 100644 --- a/KeeperSdk/src/cli/commands/sync.ts +++ b/KeeperSdk/src/cli/commands/sync.ts @@ -40,7 +40,7 @@ export const syncCommand: CliCommandDefinition = { description: ` Pulls records, folders, and related vault data into local storage. Requires an active session (login or restore-session).`, options: ' --help, -h Show this help.', - seeAlso: ' restore-session --sync, records list, folders list', + seeAlso: ' restore-session --sync, list, ls', }, async run(host, parsed) { if (wantsCliHelp(parsed)) { diff --git a/KeeperSdk/src/cli/commands/teams.ts b/KeeperSdk/src/cli/commands/teams.ts deleted file mode 100644 index 64e435d7..00000000 --- a/KeeperSdk/src/cli/commands/teams.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, KeeperCliVault, ParsedCli } from '../types' -import { getOpt, hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { formatTeamsTable, renderTeamsAsciiTable } from '../../teams/listTeams' -import { formatTeamView, teamViewTable } from '../../teams/viewTeam' - -const SUBCOMMANDS = ['list', 'view'] as const -type Sub = (typeof SUBCOMMANDS)[number] - -async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listTeams', 'teams list') - if (cap) return cap - await v.sync!() - const pattern = getOpt(parsed.opts, 'pattern') ?? null - const rows = await v.listTeams!({ pattern }) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } - } - if (rows.length === 0) { - return { - code: 0, - out: pattern ? `(no teams matched "${pattern}")\n` : '(no teams)\n', - err: '', - } - } - const table = formatTeamsTable(rows) - return { code: 0, out: renderTeamsAsciiTable(table) + '\n', err: '' } -} - -async function runView(host: KeeperCliHost, parsed: ParsedCli): Promise { - const id = parsed.positional[1] - if (!id) { - return { code: 1, out: '', err: 'teams view: missing team name or UID. Usage: teams view \n' } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'viewTeam', 'teams view') - if (cap) return cap - const view = await v.viewTeam!(id) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(view, null, 2) + '\n', err: '' } - } - const formatted = formatTeamView(view, { verbose: hasOpt(parsed.opts, 'verbose') }) - return { code: 0, out: teamViewTable(formatted) + '\n', err: '' } -} - -export const teamsCommand: CliCommandDefinition = { - name: 'teams', - order: 40, - description: 'Enterprise teams (list, view).', - usage: 'teams list|view [args] [--pattern P] [--json] [--verbose] [--help|-h]', - subcommands: [...SUBCOMMANDS], - flagOptions: ['--pattern', '--json', '--verbose'], - help: { - title: 'teams — list and inspect enterprise teams', - synopsis: ` teams list [--pattern P] [--json] - teams view NAME|UID [--json] [--verbose]`, - description: - ' Requires an enterprise account. list loads teams; view shows one team with roles and users.', - arguments: ` list Table of teams (default columns: restricts, node, user/role counts). - view Details for one team by name or team_uid.`, - options: ` --pattern list only: filter by name/uid substring. - --json Emit JSON instead of a table. - --verbose view only: include numeric node/user/role ids. - --help, -h Show this help.`, - examples: ` teams list - teams list --pattern eng - teams view "Engineering" --verbose`, - seeAlso: ' users list, sync, login', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(teamsCommand), err: '' } - } - const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub - if (!SUBCOMMANDS.includes(sub)) { - return { - code: 1, - out: '', - err: `teams: unknown subcommand "${parsed.positional[0]}". Try: teams --help\n`, - } - } - try { - switch (sub) { - case 'list': - return await runList(host, parsed) - case 'view': - return await runView(host, parsed) - } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`teams ${sub}`, e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/users.ts b/KeeperSdk/src/cli/commands/users.ts index ccc26746..032b7660 100644 --- a/KeeperSdk/src/cli/commands/users.ts +++ b/KeeperSdk/src/cli/commands/users.ts @@ -87,7 +87,7 @@ export const usersCommand: CliCommandDefinition = { examples: ` users list users list --pattern @acme.com --columns name,status,node users view user@example.com`, - seeAlso: ' teams list, sync, login', + seeAlso: ' list-team, whoami, sync, login', }, async run(host, parsed) { if (wantsCliHelp(parsed)) { diff --git a/KeeperSdk/src/cli/commands/vault.ts b/KeeperSdk/src/cli/commands/vault.ts index 9e374bbf..d3c25482 100644 --- a/KeeperSdk/src/cli/commands/vault.ts +++ b/KeeperSdk/src/cli/commands/vault.ts @@ -37,7 +37,7 @@ export const vaultCommand: CliCommandDefinition = { arguments: ' summary Print record, shared folder, team, and user-folder counts.', options: ' --json Emit JSON.\n --help, -h Show this help.', examples: ' vault summary\n vault summary --json', - seeAlso: ' sync, records list, folders tree', + seeAlso: ' sync, list, tree, whoami', }, async run(host, parsed) { if (wantsCliHelp(parsed)) { diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts index ee6a3241..8d150d61 100644 --- a/KeeperSdk/src/cli/index.ts +++ b/KeeperSdk/src/cli/index.ts @@ -1,4 +1,5 @@ import { registerBuiltinCliCommands } from './builtinCommands' +import { registerCliAlias } from './registry' let registryInitialized = false @@ -7,6 +8,7 @@ export function ensureKeeperCliRegistry(): void { if (registryInitialized) return registryInitialized = true registerBuiltinCliCommands() + registerCliAlias('?', 'help') } ensureKeeperCliRegistry() @@ -76,10 +78,6 @@ export { } from './commands/login' export { runLogoutCommand, logoutCommand } from './commands/logout' -export { recordsCommand } from './commands/records' -export { foldersCommand } from './commands/folders' -export { sharedFoldersCommand } from './commands/sharedFolders' -export { teamsCommand } from './commands/teams' export { usersCommand } from './commands/users' export { vaultCommand } from './commands/vault' export { registerDeviceCommand } from './commands/registerDevice' @@ -89,7 +87,19 @@ export { syncCommand, runVaultSync } from './commands/sync' export { getKeeperCliPromptPrefix } from './prompt' export { BUILTIN_CLI_COMMANDS, registerBuiltinCliCommands } from './builtinCommands' -export { getCommand, executeGet, registerCommanderCliCommands } from './commander' +export { + getCommand, + executeGet, + listCommand, + searchCommand, + listSfCommand, + listTeamCommand, + whoamiCommand, + lsCommand, + cdCommand, + treeCommand, + mkdirCommand, +} from './commander' export { utf8ToBase64Url, recordUid } from './utils' diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index af1d9c7c..a50889ce 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -93,10 +93,6 @@ export enum SyncErrorCode { SyncFailed = 'sync_failed', } -export enum SyncErrorCode { - SyncFailed = 'sync_failed', -} - export const ResultCodes = { INVALID_CREDENTIALS: AuthErrorCode.InvalidCredentials, MISSING_USERNAME: AuthErrorCode.MissingUsername, diff --git a/examples/sdk_example/src/utils/shellCliRestore.ts b/examples/sdk_example/src/utils/shellCliRestore.ts index bb128678..01c17f41 100644 --- a/examples/sdk_example/src/utils/shellCliRestore.ts +++ b/examples/sdk_example/src/utils/shellCliRestore.ts @@ -71,10 +71,10 @@ export async function loginViaShellCliRestoreSession( return vault } -/** Run `records list` through CLI dispatch (shell-style listing). */ +/** Run `list` through CLI dispatch (shell-style listing). */ export async function listRecordsViaShellCli(): Promise { - logger.info('[shell-cli] records list') - const result = await dispatchCliLine('records list', exampleShellCliHost) - throwOnCliFailure('records list', result) + logger.info('[shell-cli] list') + const result = await dispatchCliLine('list', exampleShellCliHost) + throwOnCliFailure('list', result) return result.out } diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts index 7fed08cf..47db657e 100644 --- a/shellcomponent/src/KeeperShell.ts +++ b/shellcomponent/src/KeeperShell.ts @@ -5,7 +5,9 @@ import xtermCss from "@xterm/xterm/css/xterm.css?inline"; import { completeCliLine } from "./cli/cliComplete.js"; import { getKeeperCliPromptPrefix } from "@keeper-security/keeper-sdk-javascript"; import { dispatchCliLine } from "./cli/cliDispatch.js"; +import { getShellKeeperHost } from "./cli/cliContext.js"; import { shellKeeperCliHost } from "./cli/keeperCliHost.js"; +import { writeShellWelcome } from "./cli/shellBanner.js"; import { setShellCliContext } from "./cli/cliContext.js"; import { loginWithCredentials, resetShellVault } from "./cli/keeperCommands.js"; @@ -609,13 +611,6 @@ export class KeeperShell extends HTMLElement { }; ensureUsableGeometry(); - requestAnimationFrame(() => { - ensureUsableGeometry(); - requestAnimationFrame(() => { - ensureUsableGeometry(); - term.focus(); - }); - }); host.tabIndex = 0; host.setAttribute("aria-label", "Command line"); @@ -954,24 +949,24 @@ export class KeeperShell extends HTMLElement { this._chain = this._chain.then(() => handleDataChunk(data)); }); - term.writeln("\x1b[1mWeb console\x1b[0m — commands run in this browser (Keeper SDK + CLI)."); - if (shellKeeperCliHost.getVault().isLoggedIn) { - term.writeln( - "Keeper Commander-style CLI: get, ls, cd, tree, search, sync-down, help (see COMMAND --help)." - ); - } else { - term.writeln( - "Not logged in — use login, restore-session, or register-device. Type help for sign-in commands." - ); - } - term.writeln("Tab completion and masked password entry are handled locally."); - term.writeln("Optional: `keeper-host` attribute (or VITE_KEEPER_HOST) for vault region."); - term.writeln("Up / Down — history; Left / Right — move cursor; Delete — forward delete."); - term.writeln( - "Ctrl+O — toggle masked input (* per character; processed locally; masked lines are not saved to history)." - ); - writeFreshPrompt(); - term.focus(); + const paintWelcome = (): void => { + writeShellWelcome((line) => term.writeln(line), { + cols: term.cols, + loggedIn: shellKeeperCliHost.getVault().isLoggedIn, + keeperHost: getShellKeeperHost(), + }); + writeFreshPrompt(); + }; + + requestAnimationFrame(() => { + ensureUsableGeometry(); + requestAnimationFrame(() => { + ensureUsableGeometry(); + paintWelcome(); + term.focus(); + }); + }); + const lateFocus = (): void => { try { fit.fit(); diff --git a/shellcomponent/src/cli/shellBanner.ts b/shellcomponent/src/cli/shellBanner.ts new file mode 100644 index 00000000..0855bfc4 --- /dev/null +++ b/shellcomponent/src/cli/shellBanner.ts @@ -0,0 +1,266 @@ +import { SdkDefaults } from "@keeper-security/keeper-sdk-javascript"; +import shellPkg from "../../package.json"; + +const ESC = { + reset: "\x1b[0m", + dim: "\x1b[2m", + yellow: "\x1b[93m", + green: "\x1b[32m", + white: "\x1b[97m", +}; + +export type ShellWelcomeOptions = { + cols: number; + loggedIn: boolean; + keeperHost?: string; + shellVersion?: string; + sdkVersion?: string; +}; + +/** Keeper lock + tagline (Commander display.welcome lines 1–7). */ +const KEEPER_ART = [ + " /#############/ /#\\ ", + " /#############/ /###\\ _ __ _______ _______ ______ _______ ______ (R)", + " /#############/ /#####\\ | | / / | ____/ | ____/ | ___ \\ | ____/ | ___ \\ ", + " /######/ \\######\\ | | / / | |____ | |____ | | __| || |____ | | __| | ", + " /######/ \\######\\ | |< < | ___/ | ___/ | |/___/ | ___/ | |/_ / ", + " /######/ \\######\\ | | \\ \\ | |_____ | |_____ | | | |_____ | | \\ \\ ", + " \\######\\ /######/ |_| \\_\\ |_______||_______||_| |_______||_| \\_\\ ", +] as const; + +const COMMANDER_LOCK_PREFIX = [ + " \\######\\ /######/", + " \\######\\ /######/ ", + " \\#############\\ \\#####/ ", + " \\#############\\ \\###/ ", + " \\#############\\ \\#/ ", +] as const; + +const COMMANDER_WORD_ART = [ + " ____ _ ", + " / ___|___ _ __ ___ _ __ ___ __ _ _ __ __| | ___ _ __ ", + " / / / _ \\| '_ ` _ \\| '_ ` _ \\ / _` | '_ \\ / _` |/ _ \\ '__| ", + " \\ \\__| (_) | | | | | | | | | | | (_| | | | | (_| | __/ | ", + " \\_____\\___/|_| |_| |_|_| |_| |_|\\__,_|_| |_|\\__,_|\\___|_| ", +] as const; + +/** Short lock-only stack for compact terminals. */ +const COMPACT_LOCK = [ + " /#############/ /#\\", + " /#############/ /###\\", + " /#############/ /#####\\", + " /######/ \\######\\", + " \\######\\ /######/", +] as const; + +/** Chars of KEEPER_ART lines that are the lock graphic (tagline is to the right). */ +const KEEPER_LOCK_CHARS = 38; + +type BannerTier = "wide" | "medium" | "compact" | "minimal"; + +const TIER_WIDE_COLS = 118; +const TIER_MEDIUM_COLS = 80; +const TIER_COMPACT_COLS = 52; + +function bannerTier(cols: number): BannerTier { + if (cols >= TIER_WIDE_COLS) return "wide"; + if (cols >= TIER_MEDIUM_COLS) return "medium"; + if (cols >= TIER_COMPACT_COLS) return "compact"; + return "minimal"; +} + +/** Word-wrap plain text to terminal width (rolls down instead of truncating). */ +export function wrapPlainText(text: string, cols: number): string[] { + const width = Math.max(cols, 16); + const trimmed = text.trim(); + if (!trimmed) return []; + if (trimmed.length <= width) return [trimmed]; + + const words = trimmed.split(/\s+/); + const lines: string[] = []; + let current = ""; + + for (const word of words) { + if (word.length > width) { + if (current) { + lines.push(current); + current = ""; + } + for (let i = 0; i < word.length; i += width) { + lines.push(word.slice(i, i + width)); + } + continue; + } + const candidate = current ? `${current} ${word}` : word; + if (candidate.length > width) { + lines.push(current); + current = word; + } else { + current = candidate; + } + } + if (current) lines.push(current); + return lines; +} + +function yellow(text: string): string { + return `${ESC.yellow}${text}${ESC.reset}`; +} + +function white(text: string): string { + return `${ESC.white}${text}${ESC.reset}`; +} + +function dim(text: string): string { + return `${ESC.dim}${text}${ESC.reset}`; +} + +function keeperLockPortion(line: string): string { + return line.slice(0, KEEPER_LOCK_CHARS).trimEnd(); +} + +function pushArtLines(out: string[], art: readonly string[], cols: number): void { + for (const line of art) { + for (const row of wrapPlainText(line, cols)) { + out.push(yellow(row)); + } + } +} + +/** Responsive Commander banner — stacks vertically on narrow terminals. */ +export function bannerLines(cols: number): string[] { + const width = Math.max(cols, 24); + const tier = bannerTier(width); + const out: string[] = []; + + if (tier === "minimal") { + out.push(yellow("Keeper Commander Shell")); + return out; + } + + if (tier === "compact") { + pushArtLines(out, COMPACT_LOCK, width); + out.push(white("Commander")); + return out; + } + + if (tier === "medium") { + for (const line of KEEPER_ART) { + const lock = keeperLockPortion(line); + if (lock) out.push(yellow(lock)); + } + for (let i = 0; i < COMMANDER_LOCK_PREFIX.length; i++) { + const lock = keeperLockPortion(COMMANDER_LOCK_PREFIX[i]); + if (lock) out.push(yellow(lock)); + const word = COMMANDER_WORD_ART[i] ?? ""; + for (const row of wrapPlainText(word, width)) { + if (row.trim()) out.push(white(row)); + } + } + return out; + } + + // wide — dual layout when each combined line fits; otherwise stack per line + for (const line of KEEPER_ART) { + for (const row of wrapPlainText(line, width)) { + out.push(yellow(row)); + } + } + for (let i = 0; i < COMMANDER_LOCK_PREFIX.length; i++) { + const left = COMMANDER_LOCK_PREFIX[i]; + const right = COMMANDER_WORD_ART[i] ?? ""; + if (left.length + right.length <= width) { + out.push(`${ESC.yellow}${left}${ESC.white}${right}${ESC.reset}`); + } else { + for (const row of wrapPlainText(left, width)) { + out.push(yellow(row)); + } + for (const row of wrapPlainText(right, width)) { + if (row.trim()) out.push(white(row)); + } + } + } + return out; +} + +function versionLine(shellVersion: string, sdkVersion: string, cols: number): string { + const label = `v${shellVersion} · SDK ${sdkVersion}`; + if (cols < 72) return dim(label); + const pad = Math.max(0, cols - label.length); + return `${ESC.dim}${" ".repeat(pad)}${label}${ESC.reset}`; +} + +function notLoggedInMessages(cols: number, keeperHost?: string): string[] { + const out: string[] = []; + out.push("You are not logged in."); + + if (keeperHost) { + out.push(...wrapPlainText(`Region: ${keeperHost}`, cols)); + } else { + for (const row of wrapPlainText( + "Set keeper-host on the element (or VITE_KEEPER_HOST) to override the default region.", + cols + )) { + out.push(dim(row)); + } + } + + if (cols >= 80) { + out.push( + `Type ${ESC.green}login ${ESC.reset} to authenticate, ` + + `${ESC.green}restore-session${ESC.reset} to resume, or ` + + `${ESC.green}register-device${ESC.reset} for token login.` + ); + } else { + out.push(`Type ${ESC.green}login ${ESC.reset} to authenticate.`); + out.push( + `${ESC.green}restore-session${ESC.reset} to resume, or ` + + `${ESC.green}register-device${ESC.reset} for token login.` + ); + } + out.push(`Type ${ESC.green}help${ESC.reset} or ${ESC.green}?${ESC.reset} for sign-in commands.`); + return out; +} + +function loggedInMessages(cols: number): string[] { + const out: string[] = [ + `Type ${ESC.green}help${ESC.reset} or ${ESC.green}?${ESC.reset} for available commands.`, + ]; + for (const row of wrapPlainText( + "Vault shell — get, ls, cd, tree, search, sync-down (see COMMAND --help).", + cols + )) { + out.push(dim(row)); + } + return out; +} + +/** Commander-style startup banner + session hint (browser shell). */ +export function buildShellWelcomeLines(options: ShellWelcomeOptions): string[] { + const cols = Math.max(options.cols, 24); + const shellVersion = options.shellVersion ?? shellPkg.version; + const sdkVersion = options.sdkVersion ?? SdkDefaults.CLIENT_VERSION; + const lines: string[] = [""]; + + lines.push(...bannerLines(cols)); + lines.push(versionLine(shellVersion, sdkVersion, cols)); + lines.push(""); + + if (!options.loggedIn) { + lines.push(...notLoggedInMessages(cols, options.keeperHost)); + } else { + lines.push(...loggedInMessages(cols)); + } + + lines.push(""); + return lines; +} + +export function writeShellWelcome( + write: (line: string) => void, + options: ShellWelcomeOptions +): void { + for (const line of buildShellWelcomeLines(options)) { + write(line); + } +} From 5ede288a68abe8c4b890a785ebb28aa1840f5add Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 8 Jun 2026 18:44:36 +0530 Subject: [PATCH 16/23] added ascii print to shell --- shellcomponent/src/cli/shellBanner.ts | 133 ++++++++++++-------------- 1 file changed, 59 insertions(+), 74 deletions(-) diff --git a/shellcomponent/src/cli/shellBanner.ts b/shellcomponent/src/cli/shellBanner.ts index 0855bfc4..01569ede 100644 --- a/shellcomponent/src/cli/shellBanner.ts +++ b/shellcomponent/src/cli/shellBanner.ts @@ -28,7 +28,24 @@ const KEEPER_ART = [ " \\######\\ /######/ |_| \\_\\ |_______||_______||_| |_______||_| \\_\\ ", ] as const; -const COMMANDER_LOCK_PREFIX = [ +/** Dual-banner rows (lock prefix + Commander word art), Commander lines 8–12. */ +const DUAL_BANNER_ROWS: ReadonlyArray = [ + [" \\######\\ /######/", " ____ _ "], + [" \\######\\ /######/ ", " / ___|___ _ __ ___ _ __ ___ __ _ _ __ __| | ___ _ __ "], + [" \\#############\\ \\#####/ ", " / / / _ \\| '_ ` _ \\| '_ ` _ \\ / _` | '_ \\ / _` |/ _ \\ '__| "], + [" \\#############\\ \\###/ ", " \\ \\__| (_) | | | | | | | | | | | (_| | | | | (_| | __/ | "], + [" \\#############\\ \\#/ ", " \\_____\\___/|_| |_| |_|_| |_| |_|\\__,_|_| |_|\\__,_|\\___|_| "], +] as const; + +/** Narrow terminal: lock graphic only (leading spaces preserved). */ +const NARROW_LOCK_ART = [ + " /#############/ /#\\ ", + " /#############/ /###\\", + " /#############/ /#####\\", + " /######/ \\######\\", + " /######/ \\######\\", + " /######/ \\######\\", + " \\######\\ /######/", " \\######\\ /######/", " \\######\\ /######/ ", " \\#############\\ \\#####/ ", @@ -36,40 +53,26 @@ const COMMANDER_LOCK_PREFIX = [ " \\#############\\ \\#/ ", ] as const; -const COMMANDER_WORD_ART = [ - " ____ _ ", - " / ___|___ _ __ ___ _ __ ___ __ _ _ __ __| | ___ _ __ ", - " / / / _ \\| '_ ` _ \\| '_ ` _ \\ / _` | '_ \\ / _` |/ _ \\ '__| ", - " \\ \\__| (_) | | | | | | | | | | | (_| | | | | (_| | __/ | ", - " \\_____\\___/|_| |_| |_|_| |_| |_|\\__,_|_| |_|\\__,_|\\___|_| ", -] as const; - -/** Short lock-only stack for compact terminals. */ -const COMPACT_LOCK = [ - " /#############/ /#\\", - " /#############/ /###\\", - " /#############/ /#####\\", - " /######/ \\######\\", - " \\######\\ /######/", -] as const; - -/** Chars of KEEPER_ART lines that are the lock graphic (tagline is to the right). */ -const KEEPER_LOCK_CHARS = 38; - -type BannerTier = "wide" | "medium" | "compact" | "minimal"; +type BannerTier = "wide" | "narrow" | "minimal"; +/** Full dual banner needs ~118 cols; below that use lock-only art. */ const TIER_WIDE_COLS = 118; -const TIER_MEDIUM_COLS = 80; -const TIER_COMPACT_COLS = 52; +const TIER_NARROW_COLS = 48; + +const COMMANDER_VERSION_ALIGN_COL = 93; function bannerTier(cols: number): BannerTier { if (cols >= TIER_WIDE_COLS) return "wide"; - if (cols >= TIER_MEDIUM_COLS) return "medium"; - if (cols >= TIER_COMPACT_COLS) return "compact"; + if (cols >= TIER_NARROW_COLS) return "narrow"; return "minimal"; } -/** Word-wrap plain text to terminal width (rolls down instead of truncating). */ +/** Truncate at terminal edge only — never trim leading spaces (ASCII art alignment). */ +function fitArtLine(line: string, cols: number): string { + return line.length > cols ? line.slice(0, cols) : line; +} + +/** Word-wrap prose only (not ASCII art). */ export function wrapPlainText(text: string, cols: number): string[] { const width = Math.max(cols, 16); const trimmed = text.trim(); @@ -115,19 +118,28 @@ function dim(text: string): string { return `${ESC.dim}${text}${ESC.reset}`; } -function keeperLockPortion(line: string): string { - return line.slice(0, KEEPER_LOCK_CHARS).trimEnd(); -} - -function pushArtLines(out: string[], art: readonly string[], cols: number): void { - for (const line of art) { - for (const row of wrapPlainText(line, cols)) { - out.push(yellow(row)); +/** Commander display.welcome dual-line logic (truncate, never reflow). */ +function pushDualRow(out: string[], left: string, right: string, cols: number): void { + let yellowLine = left; + let whiteLine = right; + if (yellowLine.length > cols) { + yellowLine = yellowLine.slice(0, cols); + } + if (yellowLine.length + whiteLine.length > cols) { + if (yellowLine.length < cols) { + whiteLine = whiteLine.slice(0, cols - yellowLine.length); + } else { + whiteLine = ""; } } + if (whiteLine) { + out.push(`${ESC.yellow}${yellowLine}${ESC.white}${whiteLine}${ESC.reset}`); + } else { + out.push(yellow(yellowLine)); + } } -/** Responsive Commander banner — stacks vertically on narrow terminals. */ +/** Responsive Commander banner — preserves fixed-width ASCII alignment. */ export function bannerLines(cols: number): string[] { const width = Math.max(cols, 24); const tier = bannerTier(width); @@ -138,55 +150,28 @@ export function bannerLines(cols: number): string[] { return out; } - if (tier === "compact") { - pushArtLines(out, COMPACT_LOCK, width); - out.push(white("Commander")); - return out; - } - - if (tier === "medium") { - for (const line of KEEPER_ART) { - const lock = keeperLockPortion(line); - if (lock) out.push(yellow(lock)); - } - for (let i = 0; i < COMMANDER_LOCK_PREFIX.length; i++) { - const lock = keeperLockPortion(COMMANDER_LOCK_PREFIX[i]); - if (lock) out.push(yellow(lock)); - const word = COMMANDER_WORD_ART[i] ?? ""; - for (const row of wrapPlainText(word, width)) { - if (row.trim()) out.push(white(row)); - } + if (tier === "narrow") { + for (const line of NARROW_LOCK_ART) { + out.push(yellow(fitArtLine(line, width))); } + out.push(white(fitArtLine("Commander", width))); return out; } - // wide — dual layout when each combined line fits; otherwise stack per line for (const line of KEEPER_ART) { - for (const row of wrapPlainText(line, width)) { - out.push(yellow(row)); - } + out.push(yellow(fitArtLine(line, width))); } - for (let i = 0; i < COMMANDER_LOCK_PREFIX.length; i++) { - const left = COMMANDER_LOCK_PREFIX[i]; - const right = COMMANDER_WORD_ART[i] ?? ""; - if (left.length + right.length <= width) { - out.push(`${ESC.yellow}${left}${ESC.white}${right}${ESC.reset}`); - } else { - for (const row of wrapPlainText(left, width)) { - out.push(yellow(row)); - } - for (const row of wrapPlainText(right, width)) { - if (row.trim()) out.push(white(row)); - } - } + for (const [left, right] of DUAL_BANNER_ROWS) { + pushDualRow(out, left, right, width); } return out; } function versionLine(shellVersion: string, sdkVersion: string, cols: number): string { const label = `v${shellVersion} · SDK ${sdkVersion}`; - if (cols < 72) return dim(label); - const pad = Math.max(0, cols - label.length); + if (cols < 48) return dim(label); + const alignTo = Math.min(COMMANDER_VERSION_ALIGN_COL, cols); + const pad = Math.max(0, alignTo - label.length); return `${ESC.dim}${" ".repeat(pad)}${label}${ESC.reset}`; } @@ -205,7 +190,7 @@ function notLoggedInMessages(cols: number, keeperHost?: string): string[] { } } - if (cols >= 80) { + if (cols >= 100) { out.push( `Type ${ESC.green}login ${ESC.reset} to authenticate, ` + `${ESC.green}restore-session${ESC.reset} to resume, or ` + From 9e76c8c99e70cc473c3fddf23d871e328cd7e44a Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Tue, 9 Jun 2026 11:04:24 +0530 Subject: [PATCH 17/23] updated readme --- shellcomponent/README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/shellcomponent/README.md b/shellcomponent/README.md index ed6a0702..f46085db 100644 --- a/shellcomponent/README.md +++ b/shellcomponent/README.md @@ -85,12 +85,15 @@ The shell dispatches the Keeper SDK CLI (`dispatchCliLine`). Built-in commands i | `login` / `logout` | Authenticate (password via masked prompt; never on the CLI line) | | `register-device` | Store device token + key for session-token login | | `restore-session` | Resume from extension session JSON (`--from-json`) | -| `sync` | Vault sync | +| `sync` | Vault sync (`sync-down`, `d`) | | `vault summary` | Record / folder / team counts | -| `records` | `list`, `get`, `find`, `share-info` | -| `folders` | `list`, `tree`, `ls`, `pwd`, `cd`, `mkdir`, `rename`, `rmdir`, `get` | -| `shared-folders` | Shared folders with optional `--verbose` counts | -| `teams` | Enterprise `list`, `view` | +| `get` | Record by UID or title (`--format`, `--unmask`) | +| `list` | Vault records (uid + title) | +| `ls`, `cd`, `tree`, `mkdir` | Folder navigation | +| `search` | Find records by title | +| `list-sf` | Shared folders (`--verbose` for counts) | +| `list-team` | Enterprise teams | +| `whoami` | Current account | | `users` | Enterprise `list`, `view` | Use ` --help` for full docs. Tab completes command names, subcommands, and flags. @@ -124,7 +127,7 @@ The dev page runs the **Keeper SDK in the browser**. To avoid CORS locally, dev ```text restore-session --from-json /dev/keeper-session.json --sync -records list +list ``` Set region on the element when needed: ``. From 51590e0617b8dbbcfc03fe80191c5e83f2c9252a Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Wed, 10 Jun 2026 10:21:09 +0530 Subject: [PATCH 18/23] refactor to show commander logo and other small changes related to command outputs formatting --- KeeperSdk/src/cli/commander/misc.ts | 72 +++++++-- KeeperSdk/src/cli/commander/nav.ts | 5 +- KeeperSdk/src/folders/folderTree.ts | 45 ++++-- KeeperSdk/src/index.ts | 1 + KeeperSdk/src/records/RecordUtils.ts | 16 ++ KeeperSdk/src/records/listRecordsTable.ts | 95 ++++++++++++ KeeperSdk/src/teams/listTeams.ts | 125 ++++++++++++++++ KeeperSdk/src/teams/shareTeams.ts | 79 ++++++++++ keeperapi/src/restMessages.ts | 20 +++ shellcomponent/src/KeeperShell.ts | 170 +++++++++++++++------- shellcomponent/src/cli/shellBanner.ts | 7 - 11 files changed, 546 insertions(+), 89 deletions(-) create mode 100644 KeeperSdk/src/records/listRecordsTable.ts create mode 100644 KeeperSdk/src/teams/shareTeams.ts diff --git a/KeeperSdk/src/cli/commander/misc.ts b/KeeperSdk/src/cli/commander/misc.ts index 5162773a..ee7babae 100644 --- a/KeeperSdk/src/cli/commander/misc.ts +++ b/KeeperSdk/src/cli/commander/misc.ts @@ -4,9 +4,10 @@ import { formatDetailedHelpForCommand } from '../help' import { ensureCapability, ensureSession } from '../commandHelpers' import { formatTable } from '../table' import { getRecordTitle } from '../../records/RecordUtils' +import { renderRecordsListTable } from '../../records/listRecordsTable' import { recordUid } from '../utils' import { formatSharedFoldersTable, renderSharedFoldersAsciiTable } from '../../sharedFolders/listSharedFolders' -import { formatTeamsTable, renderTeamsAsciiTable } from '../../teams/listTeams' +import { formatTeamsTable, renderTeamsAsciiTable, type ListTeamSort } from '../../teams/listTeams' async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { const r = await ensureSession(host) @@ -14,14 +15,15 @@ async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise [recordUid(rec), getRecordTitle(rec)]) if (hasOpt(parsed.opts, 'json')) { return { code: 0, out: JSON.stringify(records, null, 2) + '\n', err: '' } } - if (rows.length === 0) { + if (records.length === 0) { return { code: 0, out: '(no records)\n', err: '' } } - return { code: 0, out: formatTable(['record_uid', 'title'], rows), err: '' } + const verbose = hasOpt(parsed.opts, 'verbose') || hasOpt(parsed.opts, 'v') + const out = renderRecordsListTable(records, { verbose }) + '\n' + return { code: 0, out, err: '' } } async function runSearch(host: KeeperCliHost, parsed: ParsedCli): Promise { @@ -70,6 +72,8 @@ async function runListSf(host: KeeperCliHost, parsed: ParsedCli): Promise(['company', 'team_uid', 'name']) + async function runListTeam(host: KeeperCliHost, parsed: ParsedCli): Promise { const r = await ensureSession(host) if (r) return r @@ -78,14 +82,34 @@ async function runListTeam(host: KeeperCliHost, parsed: ParsedCli): Promise ({ display: row.display })) } +function folderTreeTag( + userFolder: DUserFolder | undefined, + sharedFolder: DSharedFolder | undefined, + _sharedFolderFolder: DSharedFolderFolder | undefined +): string { + if (sharedFolder) return TREE_TAG.sharedFolder + if (userFolder) return TREE_TAG.folder + return TREE_TAG.folder +} + +function formatTreeNodeName(baseName: string, tag: string, verbose: boolean, uid?: string): string { + const name = verbose && uid ? `${baseName} (${uid})` : baseName + return `${name} ${tag}` +} + +function formatTreeRecordName(title: string, verbose: boolean, recordUid?: string): string { + const name = verbose && recordUid ? `${title} (${recordUid})` : title + return `${name} ${TREE_TAG.record}` +} + type BuildOpts = Required> & { promotedRootSharedUids?: Set accountUidEmailMap: Map @@ -152,13 +178,12 @@ async function buildFolderSubtree( else if (sharedFolder) baseName = sharedFolderName(sharedFolder) else baseName = sharedFolderFolderName(sharedFolderFolder!) - let displayName = baseName - if (opts.verbose) { - displayName = `${baseName} (${folderUid})` - } - if (sharedFolder) { - displayName += ' [Shared]' - } + let displayName = formatTreeNodeName( + baseName, + folderTreeTag(userFolder, sharedFolder, sharedFolderFolder), + opts.verbose, + folderUid + ) const node: FolderTreeNode = { displayName, children: [] } @@ -188,8 +213,7 @@ async function buildFolderSubtree( node.records = records.map((recordRow) => { const record = storage.getByUid(VaultObjectKind.Record, recordRow.uid) const title = record ? getRecordTitle(record) : recordRow.name - const display = opts.verbose && record ? `${title} (${recordRow.uid}) [Record]` : `${title} [Record]` - return { display } + return { display: formatTreeRecordName(title, opts.verbose, recordRow.uid) } }) } @@ -212,8 +236,7 @@ async function buildVaultRootTree(storage: InMemoryStorage, opts: BuildOpts): Pr node.records = listed.records.map((recordRow) => { const record = storage.getByUid(VaultObjectKind.Record, recordRow.uid) const title = record ? getRecordTitle(record) : recordRow.name - const display = opts.verbose && record ? `${title} (${recordRow.uid}) [Record]` : `${title} [Record]` - return { display } + return { display: formatTreeRecordName(title, opts.verbose, recordRow.uid) } }) } return node diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 135843d1..9d1f8156 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -193,6 +193,7 @@ export { export type { ListTeamsOptions, ListTeamRow, + ListTeamSort, TeamColumnInput, FormattedTeamsTable, FormatTeamsTableOptions, diff --git a/KeeperSdk/src/records/RecordUtils.ts b/KeeperSdk/src/records/RecordUtils.ts index 15f3c744..699f5f8c 100644 --- a/KeeperSdk/src/records/RecordUtils.ts +++ b/KeeperSdk/src/records/RecordUtils.ts @@ -209,6 +209,22 @@ export function getRecordUrl(record: DRecord): string | undefined { return getRecordSummary(record).url } +/** Commander-style list description (login @ url for most record types). */ +export function getRecordDescription(record: DRecord): string { + if (record.version === 6) return 'PAM Configuration' + + const summary = getRecordSummary(record) + const parts: string[] = [] + if (summary.login) parts.push(summary.login) + if (summary.url) parts.push(summary.url) + return parts.length > 0 ? parts.join(' @ ') : '' +} + +/** Commander list column: Nested Share / Keeper Drive vs classic vault records. */ +export function getRecordCategory(record: DRecord): 'Classic' | 'Nested' { + return record.isKeeperDriveData ? 'Nested' : 'Classic' +} + const wordCache = new WeakMap() export function searchRecords(records: DRecord[], criteria: string): DRecord[] { diff --git a/KeeperSdk/src/records/listRecordsTable.ts b/KeeperSdk/src/records/listRecordsTable.ts new file mode 100644 index 00000000..17977855 --- /dev/null +++ b/KeeperSdk/src/records/listRecordsTable.ts @@ -0,0 +1,95 @@ +import type { DRecord } from '@keeper-security/keeperapi' +import { + getRecordCategory, + getRecordDescription, + getRecordTitle, + getRecordType, +} from './RecordUtils' + +const DEFAULT_COLUMN_WIDTH = 40 +const MIN_TRUNCATE_PREFIX = 3 + +export type FormattedRecordsListTable = { + headers: string[] + rows: string[][] +} + +function truncateText(text: string, maxLength: number | null): string { + if (!text) return '' + if (maxLength == null || text.length <= maxLength) return text + if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength) + return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...` +} + +function compareByTitle(recordA: DRecord, recordB: DRecord): number { + const titleA = getRecordTitle(recordA) + const titleB = getRecordTitle(recordB) + return titleA.localeCompare(titleB, undefined, { sensitivity: 'base' }) +} + +export function formatRecordsListTable( + records: DRecord[], + options: { verbose?: boolean; columnWidth?: number } = {} +): FormattedRecordsListTable { + const { verbose = false, columnWidth = DEFAULT_COLUMN_WIDTH } = options + const maxWidth = verbose ? null : columnWidth + const sorted = [...records].sort(compareByTitle) + const headers = [ + '#', + 'Record uid', + 'Type', + 'Title', + 'Description', + 'Shared', + 'Record category', + ] + const rows = sorted.map((record, index) => { + const uid = truncateText(record.uid || '(unknown uid)', maxWidth) + const type = truncateText(getRecordType(record), maxWidth) + const title = truncateText(getRecordTitle(record), maxWidth) + const description = truncateText(getRecordDescription(record), maxWidth) + const shared = record.shared ? 'True' : 'False' + const category = getRecordCategory(record) + return [String(index + 1), uid, type, title, description, shared, category] + }) + return { headers, rows } +} + +export function renderRecordsListAsciiTable( + table: FormattedRecordsListTable, + options: { minColWidth?: number } = {} +): string { + const { minColWidth = 2 } = options + const { headers, rows } = table + const columnCount = headers.length + const columnWidths: number[] = new Array(columnCount).fill(0) + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { + columnWidths[columnIndex] = Math.max(headers[columnIndex].length, minColWidth) + } + for (const row of rows) { + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { + const cell = row[columnIndex] || '' + columnWidths[columnIndex] = Math.max(columnWidths[columnIndex], cell.length, minColWidth) + } + } + const padCell = (cell: string, columnIndex: number) => + cell + ' '.repeat(columnWidths[columnIndex] - cell.length) + const formatRow = (cells: string[]) => cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') + const ruleRow = Array.from({ length: columnCount }, (_unused, columnIndex) => + '-'.repeat(columnWidths[columnIndex]) + ) + .map((dashes, columnIndex) => padCell(dashes, columnIndex)) + .join(' ') + const lines: string[] = [formatRow(headers), ruleRow] + for (const row of rows) { + lines.push(formatRow(row)) + } + return lines.join('\n') +} + +export function renderRecordsListTable( + records: DRecord[], + options: { verbose?: boolean; columnWidth?: number } = {} +): string { + return renderRecordsListAsciiTable(formatRecordsListTable(records, options)) +} diff --git a/KeeperSdk/src/teams/listTeams.ts b/KeeperSdk/src/teams/listTeams.ts index 391238aa..3d055cc5 100644 --- a/KeeperSdk/src/teams/listTeams.ts +++ b/KeeperSdk/src/teams/listTeams.ts @@ -12,6 +12,7 @@ import { type EnterpriseUser, type GetEnterpriseDataResponse, } from './enterpriseData' +import { fetchShareObjects, fetchTeamMemberEmails, resolvePrimaryEnterpriseId } from './shareTeams' export enum TeamColumn { Restricts = 'restricts', @@ -46,14 +47,27 @@ const HEADER_BY_COLUMN: Record = { [TeamColumn.Roles]: 'Roles', } +export type ListTeamSort = 'company' | 'team_uid' | 'name' + export type ListTeamsOptions = { pattern?: string | null + /** Show teams from all enterprises in contacts (Commander --all). Default: primary org only. */ + all?: boolean + /** Include Member column from enterprise cache (Commander -v). */ + verbose?: boolean + /** Fetch members via vault/get_team_members when not cached (Commander -vv). */ + veryVerbose?: boolean + sort?: ListTeamSort + /** Enterprise admin detail columns; when set, uses get_enterprise_data instead of share objects. */ columns?: TeamColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null } export type ListTeamRow = { team_uid: string name: string + company?: string + enterprise_id?: number + member?: string restricts?: string node?: string user_count?: number @@ -69,6 +83,9 @@ export type FormattedTeamsTable = { export type FormatTeamsTableOptions = { columns?: ListTeamsOptions['columns'] + /** Commander -v / -vv: show Member column. */ + showMember?: boolean + style?: 'commander' | 'enterprise' } type DecorateContext = { @@ -88,6 +105,49 @@ export function formatTeamRestricts(team: EnterpriseTeamRecord): string { } export async function listTeams(auth: Auth, options: ListTeamsOptions = {}): Promise { + if (options.columns != null) { + return listTeamsEnterprise(auth, options) + } + return listTeamsCommander(auth, options) +} + +async function listTeamsCommander(auth: Auth, options: ListTeamsOptions): Promise { + const [shareObjects, primaryEnterpriseId] = await Promise.all([ + fetchShareObjects(auth), + resolvePrimaryEnterpriseId(auth), + ]) + const showAll = options.all === true + const pattern = options.pattern?.trim() || null + const showMembers = options.verbose === true || options.veryVerbose === true + + const rows: ListTeamRow[] = [] + for (const team of shareObjects.teams.values()) { + if (!showAll && primaryEnterpriseId != null && team.enterprise_id !== primaryEnterpriseId) { + continue + } + const company = + team.enterprise_id != null + ? shareObjects.enterprises.get(team.enterprise_id) || '' + : '' + const row: ListTeamRow = { + team_uid: team.team_uid, + name: team.name, + company, + enterprise_id: team.enterprise_id, + } + if (pattern && !rowMatchesPattern(row, pattern)) continue + rows.push(row) + } + + if (showMembers) { + await attachCommanderTeamMembers(auth, rows, options.veryVerbose === true) + } + + sortCommanderRows(rows, options.sort || 'company') + return rows +} + +async function listTeamsEnterprise(auth: Auth, options: ListTeamsOptions): Promise { const columns = resolveColumns(options.columns) const includes = includesForColumns(columns) const wantsDisplayNames = columns.includes(TeamColumn.Node) || columns.includes(TeamColumn.Roles) @@ -131,6 +191,24 @@ export function formatTeamsTable( rows: ListTeamRow[], options: FormatTeamsTableOptions = {} ): FormattedTeamsTable { + const style = options.style ?? (options.columns != null ? 'enterprise' : 'commander') + if (style === 'commander') { + const showMember = options.showMember === true + const headers = ['#', 'Company', 'Team UID', 'Name'] + if (showMember) headers.push('Member') + const outRows = rows.map((row, rowIndex) => { + const cells = [ + String(rowIndex + 1), + row.company ?? '', + row.team_uid, + row.name, + ] + if (showMember) cells.push(row.member ?? '') + return cells + }) + return { headers, rows: outRows } + } + const columns = resolveColumns(options.columns) const headers: string[] = ['#', 'Team UID', 'Name', ...columns.map((column) => HEADER_BY_COLUMN[column])] @@ -229,11 +307,58 @@ function tokenize(text: string): string[] { return text.split(TOKEN_SEPARATOR_PATTERN).filter((token) => token.length > 0) } +async function attachCommanderTeamMembers( + auth: Auth, + rows: ListTeamRow[], + fetchMissing: boolean +): Promise { + const enterpriseData = new EnterpriseDataManager(auth) + const response = await enterpriseData.getData([ + EnterpriseDataInclude.TeamUsers, + EnterpriseDataInclude.Users, + ]) + const teamUsers = buildTeamUserMap(response.team_users || []) + const usernameById = buildUserUsernameMap(response.users || []) + + for (const row of rows) { + const ids = teamUsers.get(row.team_uid) + if (ids && ids.size > 0) { + row.member = resolveSortedNames(ids, usernameById).join('\n') + continue + } + if (!fetchMissing) continue + try { + const emails = await fetchTeamMemberEmails(auth, row.team_uid) + if (emails.length > 0) row.member = emails.join('\n') + } catch { + /* best-effort; leave member blank */ + } + } +} + +function sortCommanderRows(rows: ListTeamRow[], sort: ListTeamSort): void { + if (sort === 'team_uid') { + rows.sort((a, b) => a.team_uid.localeCompare(b.team_uid, undefined, { sensitivity: 'base' })) + return + } + if (sort === 'name') { + rows.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) + return + } + rows.sort((a, b) => { + const companyCmp = (a.company || '').localeCompare(b.company || '', undefined, { sensitivity: 'base' }) + if (companyCmp !== 0) return companyCmp + return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) + }) +} + function rowMatchesPattern(row: ListTeamRow, pattern: string): boolean { const lowered = pattern.toLowerCase() const tokens: string[] = [] tokens.push(row.team_uid.toLowerCase()) tokens.push(...tokenize(row.name.toLowerCase())) + if (row.company) tokens.push(...tokenize(row.company.toLowerCase())) + if (row.member) tokens.push(...tokenize(row.member.toLowerCase())) if (row.restricts) tokens.push(row.restricts.toLowerCase()) if (row.node) tokens.push(...tokenize(row.node.toLowerCase())) if (row.user_count != null) tokens.push(String(row.user_count)) diff --git a/KeeperSdk/src/teams/shareTeams.ts b/KeeperSdk/src/teams/shareTeams.ts new file mode 100644 index 00000000..0164bb95 --- /dev/null +++ b/KeeperSdk/src/teams/shareTeams.ts @@ -0,0 +1,79 @@ +import { + getShareObjectsMessage, + getTeamMembersMessage, + normal64Bytes, + webSafe64FromBytes, + type Auth, + type Records, +} from '@keeper-security/keeperapi' + +export type ShareTeamEntry = { + team_uid: string + name: string + enterprise_id?: number +} + +export type ShareObjectsSnapshot = { + enterprises: Map + teams: Map +} + +function teamUidFromShareTeam(team: Records.IShareTeam): string | null { + if (!team.teamUid) return null + if (team.teamUid instanceof Uint8Array) return webSafe64FromBytes(team.teamUid) + if (typeof team.teamUid === 'string') return team.teamUid + return null +} + +function addShareTeams(target: Map, list: Records.IShareTeam[] | null | undefined): void { + for (const team of list || []) { + const team_uid = teamUidFromShareTeam(team) + if (!team_uid) continue + target.set(team_uid, { + team_uid, + name: (team.teamname || team_uid).trim() || team_uid, + enterprise_id: team.enterpriseId ?? undefined, + }) + } +} + +/** Teams and enterprise names from vault/get_share_objects (Keeper Commander contacts cache). */ +export async function fetchShareObjects(auth: Auth): Promise { + const response = await auth.executeRest(getShareObjectsMessage({})) + const enterprises = new Map() + for (const entry of response.shareEnterpriseNames || []) { + if (entry.enterpriseId == null) continue + enterprises.set(entry.enterpriseId, (entry.enterprisename || '').trim()) + } + + const teams = new Map() + addShareTeams(teams, response.shareTeams) + addShareTeams(teams, response.shareMCTeams) + return { enterprises, teams } +} + +export async function resolvePrimaryEnterpriseId(auth: Auth): Promise { + let enterpriseId = auth.accountSummary?.license?.enterpriseId ?? undefined + if (enterpriseId != null) return enterpriseId + try { + await auth.loadAccountSummary() + } catch { + return undefined + } + return auth.accountSummary?.license?.enterpriseId ?? undefined +} + +export async function fetchTeamMemberEmails(auth: Auth, teamUid: string): Promise { + const response = await auth.executeRest( + getTeamMembersMessage({ + teamUid: normal64Bytes(teamUid), + }) + ) + const emails: string[] = [] + for (const user of response.enterpriseUser || []) { + const email = (user.email || user.enterpriseUsername || '').trim() + if (email) emails.push(email) + } + emails.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + return emails +} diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index 6b32762d..46d2ad18 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -558,6 +558,26 @@ export const getBackupMessage = ( export const getEnterprisePublicKeyMessage = (): RestOutMessage => createOutMessage('enterprise/get_enterprise_public_key', BreachWatch.EnterprisePublicKeyResponse) +export const getShareObjectsMessage = ( + data: Records.IGetShareObjectsRequest = {} +): RestMessage => + createMessage( + data, + 'vault/get_share_objects', + Records.GetShareObjectsRequest, + Records.GetShareObjectsResponse + ) + +export const getTeamMembersMessage = ( + data: Enterprise.IGetTeamMemberRequest +): RestMessage => + createMessage( + data, + 'vault/get_team_members', + Enterprise.GetTeamMemberRequest, + Enterprise.GetTeamMemberResponse + ) + export const getEnterpriseDataForUserMessage = ( data: Enterprise.IEnterpriseDataRequest ): RestMessage => diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts index 47db657e..373859e8 100644 --- a/shellcomponent/src/KeeperShell.ts +++ b/shellcomponent/src/KeeperShell.ts @@ -678,20 +678,122 @@ export class KeeperShell extends HTMLElement { } }; - const writePromptLine = (): void => { + const echoChar = (ch: string): string => (maskDisplayActive() ? "*" : ch); + + const totalDisplayChars = (lineLen: number): number => promptPrefix().length + lineLen; + + const paintPromptLine = (): void => { const line = this._lineBuf; if (cursorPos < 0) cursorPos = 0; if (cursorPos > line.length) cursorPos = line.length; const visible = maskDisplayActive() ? "*".repeat(line.length) : line; const prefix = promptPrefix(); const displayChars = prefix.length + visible.length; - clearPromptRows(); term.write(`${prefix}${visible}`); promptRows = promptDisplayRows(displayChars, term.cols); const back = line.length - cursorPos; if (back > 0) term.write(`\x1b[${back}D`); }; + const writePromptLine = (): void => { + clearPromptRows(); + paintPromptLine(); + }; + + /** Extend the line at end without clearing the prompt (typing / tab completion). */ + const applyLineAtEnd = (newLine: string): void => { + const oldLine = this._lineBuf; + if (cursorPos !== oldLine.length) { + this._lineBuf = newLine; + cursorPos = newLine.length; + writePromptLine(); + return; + } + const suffix = newLine.slice(oldLine.length); + this._lineBuf = newLine; + cursorPos = newLine.length; + if (!suffix) return; + const oldRows = promptRows; + const newRows = promptDisplayRows(totalDisplayChars(this._lineBuf.length), term.cols); + if (newRows < oldRows) { + writePromptLine(); + return; + } + term.write(maskDisplayActive() ? "*".repeat(suffix.length) : suffix); + promptRows = newRows; + }; + + /** Append at end of line without clearing the whole prompt (avoids flicker). */ + const appendAtEnd = (ch: string): void => { + const oldRows = promptRows; + this._lineBuf += ch; + cursorPos = this._lineBuf.length; + const newRows = promptDisplayRows(totalDisplayChars(this._lineBuf.length), term.cols); + if (newRows < oldRows) { + writePromptLine(); + return; + } + term.write(echoChar(ch)); + promptRows = newRows; + }; + + /** Backspace at end of line without full redraw when possible. */ + const backspaceAtEnd = (): void => { + if (cursorPos <= 0) return; + const oldRows = promptRows; + const line = this._lineBuf; + if (cursorPos < line.length) { + this._lineBuf = line.slice(0, cursorPos - 1) + line.slice(cursorPos); + cursorPos--; + writePromptLine(); + return; + } + this._lineBuf = line.slice(0, -1); + cursorPos = this._lineBuf.length; + const newRows = promptDisplayRows(totalDisplayChars(this._lineBuf.length), term.cols); + if (newRows < oldRows) { + writePromptLine(); + return; + } + term.write("\b \b"); + promptRows = newRows; + }; + + const moveCursorLeft = (): void => { + if (cursorPos <= 0) { + term.write("\x07"); + return; + } + cursorPos--; + term.write("\x1b[D"); + }; + + const moveCursorRight = (): void => { + if (cursorPos >= this._lineBuf.length) { + term.write("\x07"); + return; + } + cursorPos++; + term.write("\x1b[C"); + }; + + const insertInMiddle = (ch: string): void => { + const line = this._lineBuf; + this._lineBuf = line.slice(0, cursorPos) + ch + line.slice(cursorPos); + cursorPos++; + writePromptLine(); + }; + + const deleteForward = (): void => { + const line = this._lineBuf; + if (cursorPos >= line.length) { + term.write("\x07"); + return; + } + this._lineBuf = line.slice(0, cursorPos) + line.slice(cursorPos + 1); + writePromptLine(); + }; + const resetHistoryNav = (): void => { histFromEnd = -1; }; @@ -764,21 +866,17 @@ export class KeeperShell extends HTMLElement { return; } if (candidates.length === 1) { - this._lineBuf = base + candidates[0]; - cursorPos = this._lineBuf.length; - writePromptLine(); + applyLineAtEnd(base + candidates[0]); return; } const lcp = longestCommonPrefix(candidates); if (lcp.length > partial.length) { - this._lineBuf = base + lcp; - cursorPos = this._lineBuf.length; - writePromptLine(); + applyLineAtEnd(base + lcp); return; } term.writeln(""); term.writeln(`\x1b[90m${candidates.join(" ")}\x1b[0m`); - writePromptLine(); + paintPromptLine(); } catch { term.write("\x07"); } finally { @@ -840,61 +938,32 @@ export class KeeperShell extends HTMLElement { const handleDataChunk = async (data: string): Promise => { const tokens = feedInput(data, inputCarry); - let redrawPrompt = false; - const flushRedraw = (): void => { - if (redrawPrompt) { - writePromptLine(); - redrawPrompt = false; - } - }; for (const tok of tokens) { if (tok.k === "up") { - flushRedraw(); historyOlder(); continue; } if (tok.k === "down") { - flushRedraw(); historyNewer(); continue; } if (tok.k === "left") { bumpEditing(); - if (cursorPos > 0) { - cursorPos--; - redrawPrompt = true; - } else { - flushRedraw(); - term.write("\x07"); - } + moveCursorLeft(); continue; } if (tok.k === "right") { bumpEditing(); - if (cursorPos < this._lineBuf.length) { - cursorPos++; - redrawPrompt = true; - } else { - flushRedraw(); - term.write("\x07"); - } + moveCursorRight(); continue; } if (tok.k === "del") { bumpEditing(); - if (cursorPos < this._lineBuf.length) { - const line = this._lineBuf; - this._lineBuf = line.slice(0, cursorPos) + line.slice(cursorPos + 1); - redrawPrompt = true; - } else { - flushRedraw(); - term.write("\x07"); - } + deleteForward(); continue; } const ch = tok.v; if (ch === "\r" || ch === "\n") { - redrawPrompt = false; term.write("\r\n"); promptRows = 1; await flushLine(); @@ -902,21 +971,14 @@ export class KeeperShell extends HTMLElement { } if (ch === "\u007f" || ch === "\b") { bumpEditing(); - if (cursorPos > 0) { - const line = this._lineBuf; - this._lineBuf = line.slice(0, cursorPos - 1) + line.slice(cursorPos); - cursorPos--; - redrawPrompt = true; - } + backspaceAtEnd(); continue; } if (ch === "\t") { - flushRedraw(); await runTabComplete(); continue; } if (ch === "\x0f") { - flushRedraw(); if (pendingLoginUsername === null) { maskSensitive = !maskSensitive; } @@ -924,7 +986,6 @@ export class KeeperShell extends HTMLElement { continue; } if (ch === "\x03") { - redrawPrompt = false; this._lineBuf = ""; cursorPos = 0; pendingLoginUsername = null; @@ -938,11 +999,12 @@ export class KeeperShell extends HTMLElement { if (code < 32) continue; bumpEditing(); const line = this._lineBuf; - this._lineBuf = line.slice(0, cursorPos) + ch + line.slice(cursorPos); - cursorPos++; - redrawPrompt = true; + if (cursorPos === line.length) { + appendAtEnd(ch); + } else { + insertInMiddle(ch); + } } - flushRedraw(); }; term.onData((data) => { diff --git a/shellcomponent/src/cli/shellBanner.ts b/shellcomponent/src/cli/shellBanner.ts index 01569ede..0fd116be 100644 --- a/shellcomponent/src/cli/shellBanner.ts +++ b/shellcomponent/src/cli/shellBanner.ts @@ -181,13 +181,6 @@ function notLoggedInMessages(cols: number, keeperHost?: string): string[] { if (keeperHost) { out.push(...wrapPlainText(`Region: ${keeperHost}`, cols)); - } else { - for (const row of wrapPlainText( - "Set keeper-host on the element (or VITE_KEEPER_HOST) to override the default region.", - cols - )) { - out.push(dim(row)); - } } if (cols >= 100) { From 2e9c15ec44a76640f739e950bfe15b1cb73193d6 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 15 Jun 2026 09:04:39 +0530 Subject: [PATCH 19/23] removed shell proj --- shellcomponent/.env.example | 12 - shellcomponent/.gitignore | 6 - shellcomponent/README.md | 158 -- shellcomponent/index.html | 32 - shellcomponent/keeper-shell.d.ts | 53 - shellcomponent/package-lock.json | 1443 ----------------- shellcomponent/package.json | 55 - shellcomponent/src/KeeperShell.ts | 1105 ------------- shellcomponent/src/bufferPolyfill.ts | 6 - shellcomponent/src/cli/cliComplete.ts | 97 -- shellcomponent/src/cli/cliContext.ts | 51 - shellcomponent/src/cli/cliDispatch.ts | 23 - .../src/cli/inMemoryConfigLoader.ts | 43 - shellcomponent/src/cli/keeperCliHost.ts | 178 -- shellcomponent/src/cli/keeperCommands.ts | 26 - shellcomponent/src/cli/shellBanner.ts | 244 --- shellcomponent/src/cli/types.ts | 1 - shellcomponent/src/dev-bootstrap.ts | 34 - .../src/dev/installKeeperSameOriginProxy.ts | 66 - shellcomponent/src/dev/keeperProxyHosts.ts | 20 - shellcomponent/src/index.ts | 21 - shellcomponent/src/shims/fs-empty.ts | 13 - shellcomponent/src/shims/fs-promises-empty.ts | 21 - shellcomponent/src/shims/os-homedir.ts | 6 - shellcomponent/src/vite-env.d.ts | 14 - shellcomponent/tsconfig.json | 19 - shellcomponent/vite.config.ts | 98 -- .../vite/keeperSameOriginProxyPlugin.ts | 54 - 28 files changed, 3899 deletions(-) delete mode 100644 shellcomponent/.env.example delete mode 100644 shellcomponent/.gitignore delete mode 100644 shellcomponent/README.md delete mode 100644 shellcomponent/index.html delete mode 100644 shellcomponent/keeper-shell.d.ts delete mode 100644 shellcomponent/package-lock.json delete mode 100644 shellcomponent/package.json delete mode 100644 shellcomponent/src/KeeperShell.ts delete mode 100644 shellcomponent/src/bufferPolyfill.ts delete mode 100644 shellcomponent/src/cli/cliComplete.ts delete mode 100644 shellcomponent/src/cli/cliContext.ts delete mode 100644 shellcomponent/src/cli/cliDispatch.ts delete mode 100644 shellcomponent/src/cli/inMemoryConfigLoader.ts delete mode 100644 shellcomponent/src/cli/keeperCliHost.ts delete mode 100644 shellcomponent/src/cli/keeperCommands.ts delete mode 100644 shellcomponent/src/cli/shellBanner.ts delete mode 100644 shellcomponent/src/cli/types.ts delete mode 100644 shellcomponent/src/dev-bootstrap.ts delete mode 100644 shellcomponent/src/dev/installKeeperSameOriginProxy.ts delete mode 100644 shellcomponent/src/dev/keeperProxyHosts.ts delete mode 100644 shellcomponent/src/index.ts delete mode 100644 shellcomponent/src/shims/fs-empty.ts delete mode 100644 shellcomponent/src/shims/fs-promises-empty.ts delete mode 100644 shellcomponent/src/shims/os-homedir.ts delete mode 100644 shellcomponent/src/vite-env.d.ts delete mode 100644 shellcomponent/tsconfig.json delete mode 100644 shellcomponent/vite.config.ts delete mode 100644 shellcomponent/vite/keeperSameOriginProxyPlugin.ts diff --git a/shellcomponent/.env.example b/shellcomponent/.env.example deleted file mode 100644 index acf00699..00000000 --- a/shellcomponent/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# Optional dev-only (Vite). Copy to .env.local — never commit secrets. -# Device pre-seed (same shape as ~/.keeper/config.json); username must match `login`. -# VITE_KEEPER_DEV_DEVICE_USER=you@example.com -# VITE_KEEPER_DEV_DEVICE_TOKEN= -# VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY= - -# VITE_KEEPER_HOST=keepersecurity.eu -# VITE_KEEPER_USERNAME= -# VITE_KEEPER_SESSION_TOKEN= -# VITE_REGISTER_DEVICE_TOKEN= -# VITE_REGISTER_DEVICE_PRIVATE_KEY= -# VITE_RESTORE_SESSION_JSON= diff --git a/shellcomponent/.gitignore b/shellcomponent/.gitignore deleted file mode 100644 index c643d230..00000000 --- a/shellcomponent/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.env.local -**/.vite -**/node_modules -**/dist -**/build -**/coverage diff --git a/shellcomponent/README.md b/shellcomponent/README.md deleted file mode 100644 index f46085db..00000000 --- a/shellcomponent/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Keeper shell component (`@keeper-security/keeper-shell-component`) - -Web component that embeds the Keeper CLI in the browser: **``** and **``** (same behavior, two tag names). Commands run in-page via the Keeper JavaScript SDK and xterm.js. - -## Requirements - -- A modern browser with **custom elements** and **shadow DOM**. -- Serve the app over **HTTP(S)**. ES modules and the SDK do not run from `file://`. - -## Install - -```bash -npm install @keeper-security/keeper-shell-component -``` - -In this monorepo you can depend on the package with a **file** URL, for example `"file:../shellcomponent"`. Run **`npm run build`** in `shellcomponent` first so **`dist/`** exists for consumers that resolve the published **`exports`** entry. - -## Register the custom elements - -Import the package **once** before relying on `` or `` in the DOM. The entry module applies the **`buffer`** shim and registers both tags. - -```ts -import "@keeper-security/keeper-shell-component"; -``` - -TypeScript types are exposed via **`keeper-shell.d.ts`** (see **`package.json` → `exports`**). - -## Use in HTML - -```html - - - -``` - -Equivalent tag: - -```html - -``` - -## Use in a bundled app (Vite, webpack, etc.) - -```ts -// e.g. main.ts or app entry -import "@keeper-security/keeper-shell-component"; -``` - -Then render the tag from your framework or template. Example with JSX (tag name must be a string so the browser upgrades it to the custom element): - -```tsx -export function Page() { - return ( - - ); -} -``` - -If TypeScript complains about unknown intrinsic elements, extend **`JSX.IntrinsicElements`** for **`"web-console"`** / **`"keeper-shell"`**, or create the element with **`document.createElement("web-console")`** and attach it in a ref. - -## Attributes - -| Attribute | Description | -|-----------|-------------| -| **`height`** | Height of the terminal region (e.g. `360px`, `24rem`). Default **`320px`**. | -| **`collapsed`** | If present, the terminal panel starts hidden; only the shell toggle control is shown until the user opens it. | -| **`embed`** | If present, renders a full in-page terminal **without** the open/hide control. | -| **`keeper-host`** | Optional Keeper vault / region host override (e.g. `keepersecurity.eu`). | -| **`mask-input`** | If present, new prompts start with masked input (`*`); **Ctrl+O** toggles masking in the shell. | - -Boolean attributes follow HTML rules: include the attribute name to enable, omit to disable. - -## CLI commands (in-browser) - -The shell dispatches the Keeper SDK CLI (`dispatchCliLine`). Built-in commands include: - -| Command | Purpose | -|---------|---------| -| `help` | List commands or show help for one | -| `login` / `logout` | Authenticate (password via masked prompt; never on the CLI line) | -| `register-device` | Store device token + key for session-token login | -| `restore-session` | Resume from extension session JSON (`--from-json`) | -| `sync` | Vault sync (`sync-down`, `d`) | -| `vault summary` | Record / folder / team counts | -| `get` | Record by UID or title (`--format`, `--unmask`) | -| `list` | Vault records (uid + title) | -| `ls`, `cd`, `tree`, `mkdir` | Folder navigation | -| `search` | Find records by title | -| `list-sf` | Shared folders (`--verbose` for counts) | -| `list-team` | Enterprise teams | -| `whoami` | Current account | -| `users` | Enterprise `list`, `view` | - -Use ` --help` for full docs. Tab completes command names, subcommands, and flags. - -Write/mutate operations (add/update/delete users or teams, share record, etc.) are available on `KeeperVault` in code; CLI coverage focuses on list/get/view flows usable from the shell. - -## Programmatic API (optional) - -The package also exports helpers used by the shell (CLI dispatch, completion, vault helpers). See **`keeper-shell.d.ts`** and **`src/index.ts`** for **`dispatchCliLine`**, **`completeCliLine`**, **`setShellCliContext`**, **`resetShellVault`**, **`loginWithCredentials`**, **`loginWithSessionToken`**, and the **`KeeperShell`** / **`WebConsoleElement`** classes. - -## Layout note (non-embed) - -When the open/hide chrome is shown (**not** **`embed`**), the toggle is **`position: fixed`** at the **bottom-left** of the viewport (`12px` from edges). When the panel is visible, extra bottom padding is reserved so the terminal is not covered. Avoid placing critical fixed UI in that corner. - -## UMD bundle - -**`dist/keeper-shell.umd.cjs`** is built for environments that expect a UMD script. Wire it according to your host page’s script loader; the ESM path **`dist/keeper-shell.es.js`** is the primary **`import`** target. - -## Local development - -From **`shellcomponent/`**: - -```bash -npm install -npm run dev -``` - -Open the **`http://localhost:5175`** (or whatever port Vite prints) URL—**not** `file://`. The dev page is **`index.html`**; it loads **`src/dev-bootstrap.ts`**, which registers the shell. - -The dev page runs the **Keeper SDK in the browser**. To avoid CORS locally, dev wires a **same-origin proxy** — see [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md). Only `npm run dev` is needed. - -```text -restore-session --from-json /dev/keeper-session.json --sync -list -``` - -Set region on the element when needed: ``. - -```bash -npm run build -``` - -Runs **`build:deps`** (keeperapi + KeeperSdk) then Vite. Produces **`dist/keeper-shell.es.js`** and **`dist/keeper-shell.umd.cjs`** (~6–8 MB; Keeper SDK is bundled in-browser). - -## Production deployment (e.g. Keeper Web Vault / webpack) - -1. **Build and publish** — ship only `dist/`, `keeper-shell.d.ts`, and `package.json` `exports` (see `files` field). -2. **Import once** in your app entry: `import "@keeper-security/keeper-shell-component";` -3. **Render** `` or `` (JSX: use the string tag name `"web-console"`). -4. **Region** — set `keeper-host` when not on US prod (e.g. `keepersecurity.eu`). -5. **Networking** — the SDK calls Keeper REST/WSS from the browser. Your origin must allow it (CORS) or proxy Keeper through your app origin (same pattern as [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md), implemented in your gateway/nginx, not Vite). -6. **`restore-session --from-json`** — in production use inline JSON or an `https://` URL on your origin; local file paths work only in Vite dev (`/@fs`, `/dev/keeper-session.json`). - -The bundle is self-contained (no separate CLI server). Do not expect `remote` / `api-base` attributes — those were removed. - -## Security and networking - -Keeper API calls run from the user’s browser. Your page’s origin, CSP, and CORS (or a reverse proxy on your origin) must allow what the SDK needs. See [`docs/SAME_ORIGIN_DEV.md`](docs/SAME_ORIGIN_DEV.md) for the local dev proxy pattern. - -## License - -See **`package.json`** (`license` field). diff --git a/shellcomponent/index.html b/shellcomponent/index.html deleted file mode 100644 index 332ee899..00000000 --- a/shellcomponent/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - Web console (dev) - - - -

Web console (dev)

- - - - diff --git a/shellcomponent/keeper-shell.d.ts b/shellcomponent/keeper-shell.d.ts deleted file mode 100644 index fc807815..00000000 --- a/shellcomponent/keeper-shell.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type CliResult = { - code: number; - out: string; - err: string; - needPassword?: boolean; - loginUsername?: string; -}; - -export type ShellCliContext = { - keeperHost?: string; -}; - -export const KEEPER_SHELL_TAG: "keeper-shell"; -export const WEB_CONSOLE_TAG: "web-console"; - -export class KeeperShell extends HTMLElement { - keeperHost: string; - collapsed: boolean; - maskInput: boolean; - embed: boolean; - height: string; -} - -/** `` — extends {@link KeeperShell} (distinct class for dual registration). */ -export class WebConsoleElement extends KeeperShell {} - -/** Alias for {@link WebConsoleElement}. */ -export type WebConsole = WebConsoleElement; - -export function dispatchCliLine(line: string): Promise; -export function completeCliLine( - line: string, - options?: { loggedIn?: boolean; host?: import("@keeper-security/keeper-sdk-javascript").KeeperCliHost } -): { - base: string; - candidates: string[]; -}; - -export function setShellCliContext(next: ShellCliContext): void; -export function resetShellVault(): void; -export function loginWithCredentials(username: string, password: string): Promise; -export function loginWithSessionToken( - username: string, - sessionToken: string, - options?: { plainToken?: boolean } -): Promise; - -declare global { - interface HTMLElementTagNameMap { - "keeper-shell": KeeperShell; - "web-console": WebConsole; - } -} diff --git a/shellcomponent/package-lock.json b/shellcomponent/package-lock.json deleted file mode 100644 index 3f98f6c4..00000000 --- a/shellcomponent/package-lock.json +++ /dev/null @@ -1,1443 +0,0 @@ -{ - "name": "@keeper-security/keeper-shell-component", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@keeper-security/keeper-shell-component", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@keeper-security/keeper-sdk-javascript": "file:../KeeperSdk", - "@keeper-security/keeperapi": "file:../keeperapi", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "protobufjs": "^7.2.4" - }, - "devDependencies": { - "@types/http-proxy": "^1.17.15", - "@types/node": "^22.10.2", - "buffer": "^6.0.3", - "http-proxy": "^1.18.1", - "path-browserify": "^1.0.1", - "typescript": "~5.7.2", - "vite": "^6.0.3" - } - }, - "../keeperapi": { - "name": "@keeper-security/keeperapi", - "version": "17.2.2", - "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" - }, - "devDependencies": { - "@babel/preset-env": "^7.23.5", - "@babel/preset-typescript": "^7.23.3", - "@rollup/plugin-commonjs": "^22.0.1", - "@rollup/plugin-node-resolve": "^13.3.0", - "@types/jest": "^24.0.15", - "@types/node": "^20.9.1", - "jest": "^29.6.1", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.8.1", - "protobufjs": "^7.6.1", - "protobufjs-cli": "^1.3.1", - "rollup": "^2.79.2", - "rollup-plugin-typescript2": "^0.37.0", - "ts-jest": "^29.1.1", - "ts-node": "^8.10.2", - "typescript": "^4.0.1" - } - }, - "../KeeperSdk": { - "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@keeper-security/keeperapi": "file:../keeperapi", - "asmcrypto.js": "^2.3.2", - "ts-node": "^10.7.0", - "typescript": "^4.6.3" - }, - "devDependencies": { - "@types/node": "^25.6.0", - "prettier": "^3.8.1" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@keeper-security/keeper-sdk-javascript": { - "resolved": "../KeeperSdk", - "link": true - }, - "node_modules/@keeper-security/keeperapi": { - "resolved": "../keeperapi", - "link": true - }, - "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.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "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/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "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.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@xterm/addon-fit": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", - "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", - "license": "MIT" - }, - "node_modules/@xterm/xterm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", - "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "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/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "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", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "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": "BSD-3-Clause" - }, - "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/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/protobufjs": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", - "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", - "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.0", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.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/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - } - } -} diff --git a/shellcomponent/package.json b/shellcomponent/package.json deleted file mode 100644 index 63469686..00000000 --- a/shellcomponent/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@keeper-security/keeper-shell-component", - "version": "1.0.0", - "description": "Keeper web console: / + CLI + Keeper JavaScript SDK (in-browser).", - "type": "module", - "main": "./dist/keeper-shell.es.js", - "module": "./dist/keeper-shell.es.js", - "types": "./keeper-shell.d.ts", - "exports": { - ".": { - "types": "./keeper-shell.d.ts", - "import": "./dist/keeper-shell.es.js", - "require": "./dist/keeper-shell.umd.cjs" - } - }, - "files": [ - "dist", - "keeper-shell.d.ts" - ], - "scripts": { - "clean": "rm -rf dist node_modules", - "rebuild": "npm run clean && npm install && npm run build:deps && npm run build", - "build:deps": "npm --prefix ../keeperapi run build && npm --prefix ../KeeperSdk run build", - "build:local-keeper-sdk": "npm --prefix ../KeeperSdk install && npm --prefix ../KeeperSdk run build", - "dev": "npm install && vite", - "dev:inspect": "npm install && node --inspect=9229 ./node_modules/vite/bin/vite.js", - "build": "npm run build:deps && vite build", - "prepublishOnly": "npm run build", - "preview": "vite preview", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@keeper-security/keeper-sdk-javascript": "file:../KeeperSdk", - "@keeper-security/keeperapi": "file:../keeperapi", - "protobufjs": "^7.2.4", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "buffer": "^6.0.3", - "http-proxy": "^1.18.1", - "@types/http-proxy": "^1.17.15", - "path-browserify": "^1.0.1", - "typescript": "~5.7.2", - "vite": "^6.0.3" - }, - "keywords": [ - "keeper", - "web-components", - "cli", - "xterm" - ], - "license": "ISC" -} diff --git a/shellcomponent/src/KeeperShell.ts b/shellcomponent/src/KeeperShell.ts deleted file mode 100644 index 373859e8..00000000 --- a/shellcomponent/src/KeeperShell.ts +++ /dev/null @@ -1,1105 +0,0 @@ -import { Terminal } from "@xterm/xterm"; -import { FitAddon } from "@xterm/addon-fit"; -import xtermCss from "@xterm/xterm/css/xterm.css?inline"; - -import { completeCliLine } from "./cli/cliComplete.js"; -import { getKeeperCliPromptPrefix } from "@keeper-security/keeper-sdk-javascript"; -import { dispatchCliLine } from "./cli/cliDispatch.js"; -import { getShellKeeperHost } from "./cli/cliContext.js"; -import { shellKeeperCliHost } from "./cli/keeperCliHost.js"; -import { writeShellWelcome } from "./cli/shellBanner.js"; -import { setShellCliContext } from "./cli/cliContext.js"; -import { loginWithCredentials, resetShellVault } from "./cli/keeperCommands.js"; - -export const KEEPER_SHELL_TAG = "keeper-shell"; -/** Legacy custom element name (same behavior as {@link KEEPER_SHELL_TAG}). */ -export const WEB_CONSOLE_TAG = "web-console"; - -const ATTR_KEEPER_HOST = "keeper-host"; -const ATTR_COLLAPSED = "collapsed"; -const ATTR_HEIGHT = "height"; -/** Full in-page terminal only (no show/hide button). */ -const ATTR_EMBED = "embed"; -/** When set, each new `$ ` prompt starts with masked input (`*` display). Toggle with Ctrl+O. */ -const ATTR_MASK_INPUT = "mask-input"; - -/** Same icon for both states; `aria-label` / `title` reflect expand vs collapse. */ -function setConsoleToggleButtonUi(btn: HTMLButtonElement, collapsed: boolean): void { - const label = collapsed ? "Open console" : "Hide console"; - btn.setAttribute("aria-label", label); - btn.title = label; - btn.innerHTML = - ''; -} - -type CliResponse = { - out?: string; - err?: string; - error?: string; - code?: number; - needPassword?: boolean; - loginUsername?: string; -}; - -type CompleteResponse = { base?: unknown; candidates?: unknown }; - -function formatErr(e: unknown): string { - return e instanceof Error ? e.message : String(e); -} - -function longestCommonPrefix(values: string[]): string { - if (values.length === 0) return ""; - let pref = values[0]; - for (let i = 1; i < values.length; i++) { - const s = values[i]; - let j = 0; - const n = Math.min(pref.length, s.length); - while (j < n && pref[j] === s[j]) j++; - pref = pref.slice(0, j); - if (pref === "") break; - } - return pref; -} - -/** Fallback when SDK prompt helper is unavailable. */ -const FALLBACK_PROMPT_PREFIX = "$ "; - -/** Screen rows used by prompt + input when the line wraps (cols from xterm). */ -function promptDisplayRows(displayChars: number, cols: number): number { - const width = Math.max(cols, 1); - if (displayChars <= 0) return 1; - return Math.ceil(displayChars / width); -} - -/** Parsed stdin tokens from xterm (arrow keys arrive as ESC sequences). */ -type InputTok = - | { k: "c"; v: string } - | { k: "up" } - | { k: "down" } - | { k: "left" } - | { k: "right" } - | { k: "del" }; - -function feedInput(chunk: string, carry: { s: string }): InputTok[] { - const s = carry.s + chunk; - carry.s = ""; - const out: InputTok[] = []; - let i = 0; - while (i < s.length) { - if (s.charCodeAt(i) !== 0x1b) { - out.push({ k: "c", v: s[i] }); - i++; - continue; - } - if (i + 1 >= s.length) { - carry.s = s.slice(i); - break; - } - const c1 = s[i + 1]; - if (c1 === "[") { - let j = i + 2; - let foundFinal = false; - while (j < s.length) { - const code = s.charCodeAt(j); - if (code >= 0x40 && code <= 0x7e) { - foundFinal = true; - const fn = s[j]; - const param = s.slice(i + 2, j); - if (fn === "A") out.push({ k: "up" }); - else if (fn === "B") out.push({ k: "down" }); - else if (fn === "C") out.push({ k: "right" }); - else if (fn === "D") out.push({ k: "left" }); - else if (fn === "~" && param === "3") out.push({ k: "del" }); - i = j + 1; - break; - } - j++; - } - if (!foundFinal) { - carry.s = s.slice(i); - break; - } - continue; - } - if (c1 === "O") { - if (i + 2 >= s.length) { - carry.s = s.slice(i); - break; - } - const c2 = s[i + 2]; - if (c2 === "A") out.push({ k: "up" }); - else if (c2 === "B") out.push({ k: "down" }); - else if (c2 === "C") out.push({ k: "right" }); - else if (c2 === "D") out.push({ k: "left" }); - else { - out.push({ k: "c", v: "\x1b" }); - i++; - continue; - } - i += 3; - continue; - } - out.push({ k: "c", v: "\x1b" }); - i++; - } - return out; -} - -export class KeeperShell extends HTMLElement { - static observedAttributes = [ - ATTR_KEEPER_HOST, - ATTR_COLLAPSED, - ATTR_HEIGHT, - ATTR_EMBED, - ATTR_MASK_INPUT, - ]; - - private _term: Terminal | null = null; - private _fit: FitAddon | null = null; - private _ro: ResizeObserver | null = null; - private _onWinResize: (() => void) | null = null; - private _chain: Promise = Promise.resolve(); - private _lineBuf = ""; - private _started = false; - private _completing = false; - private _inputListeners: AbortController | null = null; - /** Last `keeper-host` synced to {@link resetShellVault}; `undefined` = never synced. */ - private _syncedKeeperHostKey: string | undefined; - - constructor() { - super(); - // Lets the browser delegate focus to the xterm textarea inside shadow (keyboard input). - this.attachShadow({ mode: "open", delegatesFocus: true }); - } - - get keeperHost(): string { - return (this.getAttribute(ATTR_KEEPER_HOST) || "").trim(); - } - - set keeperHost(v: string) { - if (!v.trim()) this.removeAttribute(ATTR_KEEPER_HOST); - else this.setAttribute(ATTR_KEEPER_HOST, v); - } - - get collapsed(): boolean { - return this.hasAttribute(ATTR_COLLAPSED); - } - - set collapsed(v: boolean) { - if (v) this.setAttribute(ATTR_COLLAPSED, ""); - else this.removeAttribute(ATTR_COLLAPSED); - } - - get maskInput(): boolean { - return this.hasAttribute(ATTR_MASK_INPUT); - } - - set maskInput(v: boolean) { - if (v) this.setAttribute(ATTR_MASK_INPUT, ""); - else this.removeAttribute(ATTR_MASK_INPUT); - } - - /** Browser CLI layout: terminal fills the element; no collapsible chrome. */ - get embed(): boolean { - return this.hasAttribute(ATTR_EMBED); - } - - set embed(v: boolean) { - if (v) this.setAttribute(ATTR_EMBED, ""); - else this.removeAttribute(ATTR_EMBED); - } - - private _isEmbed(): boolean { - return this.hasAttribute(ATTR_EMBED); - } - - private _syncShellContext(): void { - const h = this.getAttribute(ATTR_KEEPER_HOST)?.trim() || ""; - setShellCliContext({ keeperHost: h || undefined }); - if (this._syncedKeeperHostKey !== h) { - this._syncedKeeperHostKey = h; - resetShellVault(); - } - } - - attributeChangedCallback(name: string, _old: string | null, val: string | null): void { - if (name === ATTR_HEIGHT && this.shadowRoot && !this._isEmbed()) { - const host = this.shadowRoot.querySelector(".wc-terminal-host"); - if (host instanceof HTMLElement) host.style.height = val || "320px"; - } - if (name === ATTR_KEEPER_HOST) { - this._syncShellContext(); - } - if (name === ATTR_EMBED) { - const shouldShow = this._isEmbed() || !this.collapsed; - if (this._started) this._teardownTerminal(); - this._renderShell(); - this._wireChrome(); - if (shouldShow) this._mountTerminal(); - return; - } - if (name === ATTR_COLLAPSED && !this._isEmbed() && this.shadowRoot) { - if (this.collapsed) this._hidePanel(); - else this._showPanel(); - } - } - - connectedCallback(): void { - if (!this.shadowRoot) return; - while (this.firstChild) { - this.removeChild(this.firstChild); - } - // Do not make the host a tab stop — xterm's inner textarea (`.wc-terminal-host`, tabindex=0) must receive keys. - this.tabIndex = -1; - this.setAttribute("role", "application"); - this.setAttribute("aria-label", "Keeper CLI"); - this._syncShellContext(); - this._renderShell(); - this._wireChrome(); - if (this._isEmbed() || !this.collapsed) { - this._mountTerminal(); - } - } - - disconnectedCallback(): void { - this._teardownTerminal(); - resetShellVault(); - this._syncedKeeperHostKey = undefined; - } - - private _wireChrome(): void { - if (!this.shadowRoot || this._isEmbed()) return; - const btn = this.shadowRoot.querySelector(".wc-toggle"); - if (!(btn instanceof HTMLButtonElement)) return; - btn.addEventListener("click", () => this._toggle()); - } - - private _toggle(): void { - if (this._isEmbed()) return; - const panel = this.shadowRoot?.querySelector(".wc-panel"); - if (!(panel instanceof HTMLElement)) return; - if (panel.hasAttribute("hidden")) { - this.removeAttribute(ATTR_COLLAPSED); - this._showPanel(); - } else { - this.setAttribute(ATTR_COLLAPSED, ""); - this._hidePanel(); - } - } - - /** Hide the terminal panel but keep xterm session (scrollback, history, current line). */ - private _hidePanel(): void { - const panel = this.shadowRoot?.querySelector(".wc-panel"); - const btn = this.shadowRoot?.querySelector(".wc-toggle"); - if (panel instanceof HTMLElement) panel.setAttribute("hidden", ""); - if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, true); - if (this._term) this._term.options.cursorBlink = false; - this._applyHostShellMinHeight(); - } - - /** Show the panel; mount on first open, otherwise refit and focus the existing terminal. */ - private _showPanel(): void { - const panel = this.shadowRoot?.querySelector(".wc-panel"); - const btn = this.shadowRoot?.querySelector(".wc-toggle"); - if (panel instanceof HTMLElement) panel.removeAttribute("hidden"); - if (btn instanceof HTMLButtonElement) setConsoleToggleButtonUi(btn, false); - if (!this._started) { - this._mountTerminal(); - } else { - this._resumeTerminal(); - } - this._applyHostShellMinHeight(); - } - - private _resumeTerminal(): void { - const term = this._term; - const fit = this._fit; - if (!term || !fit) return; - term.options.cursorBlink = true; - const refit = (): void => { - try { - fit.fit(); - if (term.cols < 2 || term.rows < 1) { - term.resize(80, 24); - } - term.focus(); - } catch { - /* detached or zero-size host */ - } - }; - requestAnimationFrame(() => { - refit(); - requestAnimationFrame(refit); - }); - } - - private _applyHostShellMinHeight(): void { - if (this._isEmbed()) return; - const panel = this.shadowRoot?.querySelector(".wc-panel"); - const panelHidden = panel?.hasAttribute("hidden"); - const h = (this.getAttribute(ATTR_HEIGHT) || "320px").trim(); - if (panelHidden) { - this.style.minHeight = "auto"; - return; - } - if (/^\d+(\.\d+)?px$/.test(h) || /^\d+(\.\d+)?rem$/.test(h)) { - this.style.minHeight = `calc(${h} + 3.75rem)`; - } else { - this.style.minHeight = "min(90vh, 28rem)"; - } - } - - private _renderShell(): void { - const height = this.getAttribute(ATTR_HEIGHT) || "320px"; - if (!this.shadowRoot) return; - this.shadowRoot.innerHTML = ""; - - const style = document.createElement("style"); - const embed = this._isEmbed(); - - if (embed) { - style.textContent = ` - ${xtermCss} - :host { - display: flex; - flex-direction: column; - width: 100%; - min-height: ${height}; - box-sizing: border-box; - font-family: system-ui, sans-serif; - pointer-events: auto; - } - .wc-root { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - padding: 0; - pointer-events: auto; - } - .wc-panel { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - margin: 0; - pointer-events: auto; - border: 2px solid #3d3d3d; - border-radius: 0; - overflow: hidden; - background: #121212 !important; - background-color: #121212 !important; - } - .wc-cli-cap { - flex-shrink: 0; - padding: 6px 12px; - font-size: 12px; - font-weight: 600; - color: #c8c8c8; - background: #1e1e1e !important; - border-bottom: 1px solid #404040; - user-select: none; - } - .wc-terminal-host { - flex: 1; - min-height: ${height}; - min-width: 280px; - width: 100%; - box-sizing: border-box; - padding: 6px 0; - position: relative; - cursor: text; - outline: none; - background: #0b0b0b !important; - background-color: #0b0b0b !important; - pointer-events: auto; - } - .wc-terminal-host .xterm { height: 100%; background-color: #0b0b0b !important; pointer-events: auto; } - .wc-terminal-host .xterm-viewport { background-color: #0b0b0b !important; } - .wc-terminal-host .xterm-screen { background-color: #0b0b0b !important; } - `; - } else { - style.textContent = ` - ${xtermCss} - :host { - display: block; - width: 100%; - box-sizing: border-box; - font-family: system-ui, sans-serif; - min-height: calc(${height} + 3.75rem); - pointer-events: auto; - } - .wc-root { - padding: 12px; - display: flex; - flex-direction: column; - gap: 0; - pointer-events: auto; - } - .wc-root:has(.wc-panel:not([hidden])) { - padding-bottom: 48px; - } - .wc-toggle { - box-sizing: border-box; - width: 30px; - height: 30px; - padding: 0; - margin: 0; - display: inline-flex; - align-items: center; - justify-content: center; - line-height: 0; - cursor: pointer; - border-radius: 6px; - border: 1px solid #c4c4c4; - background: #f0f0f0; - color: #2a2a2a; - flex-shrink: 0; - position: fixed; - bottom: 12px; - left: 12px; - z-index: 10000; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); - } - .wc-toggle:hover { - background: #e4e4e4; - border-color: #a8a8a8; - } - .wc-toggle:focus-visible { - outline: 2px solid #2563eb; - outline-offset: 2px; - } - .wc-toggle-icon { - display: block; - width: 22px; - height: 22px; - object-fit: contain; - flex-shrink: 0; - pointer-events: none; - user-select: none; - } - .wc-panel { - margin-top: 0; - display: flex; - flex-direction: column; - border: 2px solid #3d3d3d; - border-radius: 10px; - overflow: hidden; - background: #121212 !important; - background-color: #121212 !important; - box-shadow: 0 2px 12px rgba(0,0,0,0.25); - pointer-events: auto; - } - .wc-panel[hidden] { display: none; } - .wc-cli-cap { - flex-shrink: 0; - padding: 6px 12px; - font-size: 12px; - font-weight: 600; - color: #c8c8c8; - background: #1e1e1e !important; - border-bottom: 1px solid #404040; - user-select: none; - } - .wc-terminal-host { - flex: 1; - min-height: ${height}; - min-width: 280px; - width: 100%; - box-sizing: border-box; - padding: 6px 0; - position: relative; - cursor: text; - outline: none; - background: #0b0b0b !important; - background-color: #0b0b0b !important; - pointer-events: auto; - } - .wc-terminal-host .xterm { height: 100%; background-color: #0b0b0b !important; pointer-events: auto; } - .wc-terminal-host .xterm-viewport { background-color: #0b0b0b !important; } - .wc-terminal-host .xterm-screen { background-color: #0b0b0b !important; } - `; - } - - const root = document.createElement("div"); - root.className = "wc-root"; - - const panel = document.createElement("div"); - panel.className = "wc-panel"; - if (!embed && this.collapsed) panel.setAttribute("hidden", ""); - - const cap = document.createElement("div"); - cap.className = "wc-cli-cap"; - cap.textContent = "Keeper CLI — in-browser SDK (click in the terminal, then type)"; - - const host = document.createElement("div"); - host.className = "wc-terminal-host"; - if (!embed) { - host.style.height = height; - } - host.style.backgroundColor = "#0b0b0b"; - - panel.appendChild(cap); - panel.appendChild(host); - - if (!embed) { - const toggle = document.createElement("button"); - toggle.type = "button"; - toggle.className = "wc-toggle"; - setConsoleToggleButtonUi(toggle, this.collapsed); - root.appendChild(panel); - root.appendChild(toggle); - } else { - root.appendChild(panel); - } - this.shadowRoot.append(style, root); - - if (!embed) { - this.style.display = "block"; - this.style.width = "100%"; - this.style.boxSizing = "border-box"; - this._applyHostShellMinHeight(); - } else { - this.style.minHeight = "100%"; - this.style.display = "block"; - this.style.width = "100%"; - this.style.boxSizing = "border-box"; - } - } - - private _mountTerminal(): void { - if (this._started) return; - const host = this.shadowRoot?.querySelector(".wc-terminal-host"); - if (!(host instanceof HTMLElement)) return; - - const term = new Terminal({ - allowTransparency: false, - cursorBlink: true, - convertEol: true, - fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', - fontSize: 14, - theme: { - background: "#0b0b0b", - foreground: "#eaeaea", - cursor: "#eaeaea", - }, - }); - - const fit = new FitAddon(); - term.loadAddon(fit); - term.open(host); - - const refit = (): void => { - try { - fit.fit(); - } catch { - /* layout not ready */ - } - }; - - /** xterm FitAddon yields 0×0 if the container has no layout box yet. */ - const ensureUsableGeometry = (): void => { - refit(); - if (term.cols < 2 || term.rows < 1) { - try { - term.resize(80, 24); - } catch { - /* ignore */ - } - } - }; - - ensureUsableGeometry(); - - host.tabIndex = 0; - host.setAttribute("aria-label", "Command line"); - const focusTerm = (): void => { - term.focus(); - }; - this._inputListeners?.abort(); - this._inputListeners = new AbortController(); - const sig = this._inputListeners.signal; - host.addEventListener("pointerdown", focusTerm, { signal: sig }); - host.addEventListener("focusin", focusTerm, { signal: sig }); - this.addEventListener( - "focusin", - (ev) => { - if (ev.target === this) focusTerm(); - }, - { signal: sig } - ); - - this._term = term; - this._fit = fit; - this._started = true; - this._lineBuf = ""; - - const HISTORY_MAX = 500; - const history: string[] = []; - const inputCarry = { s: "" }; - let histFromEnd = -1; - let histDraft = ""; - let cursorPos = 0; - /** Rows on screen for the current prompt line (increases when input wraps). */ - let promptRows = 1; - let maskSensitive = this.hasAttribute(ATTR_MASK_INPUT); - let pendingLoginUsername: string | null = null; - - const maskDisplayActive = (): boolean => - maskSensitive || pendingLoginUsername !== null; - - const resetMaskAfterPasswordFlow = (): void => { - maskSensitive = this.hasAttribute(ATTR_MASK_INPUT); - }; - - const afterNewPrompt = (): void => { - if (this.hasAttribute(ATTR_MASK_INPUT)) maskSensitive = true; - }; - - const promptPrefix = (): string => { - try { - return getKeeperCliPromptPrefix(shellKeeperCliHost); - } catch { - return FALLBACK_PROMPT_PREFIX; - } - }; - - const writeFreshPrompt = (): void => { - promptRows = 1; - term.write(promptPrefix()); - afterNewPrompt(); - }; - - const clearPromptRows = (): void => { - term.write("\r\x1b[2K"); - for (let r = 1; r < promptRows; r++) { - term.write("\x1b[1A\r\x1b[2K"); - } - }; - - const echoChar = (ch: string): string => (maskDisplayActive() ? "*" : ch); - - const totalDisplayChars = (lineLen: number): number => promptPrefix().length + lineLen; - - const paintPromptLine = (): void => { - const line = this._lineBuf; - if (cursorPos < 0) cursorPos = 0; - if (cursorPos > line.length) cursorPos = line.length; - const visible = maskDisplayActive() ? "*".repeat(line.length) : line; - const prefix = promptPrefix(); - const displayChars = prefix.length + visible.length; - term.write(`${prefix}${visible}`); - promptRows = promptDisplayRows(displayChars, term.cols); - const back = line.length - cursorPos; - if (back > 0) term.write(`\x1b[${back}D`); - }; - - const writePromptLine = (): void => { - clearPromptRows(); - paintPromptLine(); - }; - - /** Extend the line at end without clearing the prompt (typing / tab completion). */ - const applyLineAtEnd = (newLine: string): void => { - const oldLine = this._lineBuf; - if (cursorPos !== oldLine.length) { - this._lineBuf = newLine; - cursorPos = newLine.length; - writePromptLine(); - return; - } - const suffix = newLine.slice(oldLine.length); - this._lineBuf = newLine; - cursorPos = newLine.length; - if (!suffix) return; - const oldRows = promptRows; - const newRows = promptDisplayRows(totalDisplayChars(this._lineBuf.length), term.cols); - if (newRows < oldRows) { - writePromptLine(); - return; - } - term.write(maskDisplayActive() ? "*".repeat(suffix.length) : suffix); - promptRows = newRows; - }; - - /** Append at end of line without clearing the whole prompt (avoids flicker). */ - const appendAtEnd = (ch: string): void => { - const oldRows = promptRows; - this._lineBuf += ch; - cursorPos = this._lineBuf.length; - const newRows = promptDisplayRows(totalDisplayChars(this._lineBuf.length), term.cols); - if (newRows < oldRows) { - writePromptLine(); - return; - } - term.write(echoChar(ch)); - promptRows = newRows; - }; - - /** Backspace at end of line without full redraw when possible. */ - const backspaceAtEnd = (): void => { - if (cursorPos <= 0) return; - const oldRows = promptRows; - const line = this._lineBuf; - if (cursorPos < line.length) { - this._lineBuf = line.slice(0, cursorPos - 1) + line.slice(cursorPos); - cursorPos--; - writePromptLine(); - return; - } - this._lineBuf = line.slice(0, -1); - cursorPos = this._lineBuf.length; - const newRows = promptDisplayRows(totalDisplayChars(this._lineBuf.length), term.cols); - if (newRows < oldRows) { - writePromptLine(); - return; - } - term.write("\b \b"); - promptRows = newRows; - }; - - const moveCursorLeft = (): void => { - if (cursorPos <= 0) { - term.write("\x07"); - return; - } - cursorPos--; - term.write("\x1b[D"); - }; - - const moveCursorRight = (): void => { - if (cursorPos >= this._lineBuf.length) { - term.write("\x07"); - return; - } - cursorPos++; - term.write("\x1b[C"); - }; - - const insertInMiddle = (ch: string): void => { - const line = this._lineBuf; - this._lineBuf = line.slice(0, cursorPos) + ch + line.slice(cursorPos); - cursorPos++; - writePromptLine(); - }; - - const deleteForward = (): void => { - const line = this._lineBuf; - if (cursorPos >= line.length) { - term.write("\x07"); - return; - } - this._lineBuf = line.slice(0, cursorPos) + line.slice(cursorPos + 1); - writePromptLine(); - }; - - const resetHistoryNav = (): void => { - histFromEnd = -1; - }; - - const bumpEditing = (): void => { - histFromEnd = -1; - }; - - const pushHistoryEntry = (cmd: string): void => { - if (!cmd) return; - history.push(cmd); - if (history.length > HISTORY_MAX) history.shift(); - }; - - const historyOlder = (): void => { - if (history.length === 0) { - term.write("\x07"); - return; - } - if (histFromEnd === -1) { - histDraft = this._lineBuf; - histFromEnd = 0; - } else if (histFromEnd < history.length - 1) { - histFromEnd++; - } else { - term.write("\x07"); - return; - } - this._lineBuf = history[history.length - 1 - histFromEnd] ?? ""; - cursorPos = this._lineBuf.length; - writePromptLine(); - }; - - const historyNewer = (): void => { - if (histFromEnd === -1) { - return; - } - if (histFromEnd > 0) { - histFromEnd--; - this._lineBuf = history[history.length - 1 - histFromEnd] ?? ""; - cursorPos = this._lineBuf.length; - writePromptLine(); - return; - } - histFromEnd = -1; - this._lineBuf = histDraft; - cursorPos = this._lineBuf.length; - writePromptLine(); - }; - - const runTabComplete = async (): Promise => { - if (pendingLoginUsername !== null) { - term.write("\x07"); - return; - } - if (this._completing) return; - this._completing = true; - try { - bumpEditing(); - const data = completeCliLine(this._lineBuf, { host: shellKeeperCliHost }) as CompleteResponse; - const base = typeof data.base === "string" ? data.base : ""; - const raw = data.candidates; - const candidates = Array.isArray(raw) - ? raw.filter((x): x is string => typeof x === "string") - : []; - const partial = this._lineBuf.slice(base.length); - - if (candidates.length === 0) { - term.write("\x07"); - return; - } - if (candidates.length === 1) { - applyLineAtEnd(base + candidates[0]); - return; - } - const lcp = longestCommonPrefix(candidates); - if (lcp.length > partial.length) { - applyLineAtEnd(base + lcp); - return; - } - term.writeln(""); - term.writeln(`\x1b[90m${candidates.join(" ")}\x1b[0m`); - paintPromptLine(); - } catch { - term.write("\x07"); - } finally { - this._completing = false; - } - }; - - const flushLine = async (): Promise => { - const cmd = this._lineBuf.trim(); - const skipHistory = maskDisplayActive(); - this._lineBuf = ""; - cursorPos = 0; - resetHistoryNav(); - histDraft = ""; - - if (pendingLoginUsername !== null) { - if (!cmd) { - term.writeln("\x1b[31mlogin: password required\x1b[0m"); - writeFreshPrompt(); - return; - } - const username = pendingLoginUsername; - pendingLoginUsername = null; - term.writeln("\x1b[90mSigning in…\x1b[0m"); - try { - const data = await loginWithCredentials(username, cmd); - if (data.out) term.write(data.out); - if (data.err) term.write(`\x1b[31m${data.err}\x1b[0m`); - } catch (err) { - term.write(`\x1b[31mError: ${formatErr(err)}\x1b[0m`); - } - resetMaskAfterPasswordFlow(); - writeFreshPrompt(); - return; - } - - if (!cmd) { - writeFreshPrompt(); - return; - } - if (!skipHistory) pushHistoryEntry(cmd); - - try { - const data = await dispatchCliLine(cmd); - if (data.needPassword === true && typeof data.loginUsername === "string") { - pendingLoginUsername = data.loginUsername; - maskSensitive = true; - term.writeln("\x1b[90mPassword (masked):\x1b[0m"); - writeFreshPrompt(); - return; - } - if (data.out) term.write(data.out); - if (data.err) term.write(`\x1b[31m${data.err}\x1b[0m`); - } catch (err) { - term.write(`\x1b[31mError: ${formatErr(err)}\x1b[0m`); - } - writeFreshPrompt(); - }; - - const handleDataChunk = async (data: string): Promise => { - const tokens = feedInput(data, inputCarry); - for (const tok of tokens) { - if (tok.k === "up") { - historyOlder(); - continue; - } - if (tok.k === "down") { - historyNewer(); - continue; - } - if (tok.k === "left") { - bumpEditing(); - moveCursorLeft(); - continue; - } - if (tok.k === "right") { - bumpEditing(); - moveCursorRight(); - continue; - } - if (tok.k === "del") { - bumpEditing(); - deleteForward(); - continue; - } - const ch = tok.v; - if (ch === "\r" || ch === "\n") { - term.write("\r\n"); - promptRows = 1; - await flushLine(); - continue; - } - if (ch === "\u007f" || ch === "\b") { - bumpEditing(); - backspaceAtEnd(); - continue; - } - if (ch === "\t") { - await runTabComplete(); - continue; - } - if (ch === "\x0f") { - if (pendingLoginUsername === null) { - maskSensitive = !maskSensitive; - } - writePromptLine(); - continue; - } - if (ch === "\x03") { - this._lineBuf = ""; - cursorPos = 0; - pendingLoginUsername = null; - resetHistoryNav(); - histDraft = ""; - term.write("^C\r\n"); - writeFreshPrompt(); - continue; - } - const code = ch.charCodeAt(0); - if (code < 32) continue; - bumpEditing(); - const line = this._lineBuf; - if (cursorPos === line.length) { - appendAtEnd(ch); - } else { - insertInMiddle(ch); - } - } - }; - - term.onData((data) => { - this._chain = this._chain.then(() => handleDataChunk(data)); - }); - - const paintWelcome = (): void => { - writeShellWelcome((line) => term.writeln(line), { - cols: term.cols, - loggedIn: shellKeeperCliHost.getVault().isLoggedIn, - keeperHost: getShellKeeperHost(), - }); - writeFreshPrompt(); - }; - - requestAnimationFrame(() => { - ensureUsableGeometry(); - requestAnimationFrame(() => { - ensureUsableGeometry(); - paintWelcome(); - term.focus(); - }); - }); - - const lateFocus = (): void => { - try { - fit.fit(); - if (term.cols < 2 || term.rows < 1) { - term.resize(80, 24); - } - term.focus(); - } catch { - /* ignore */ - } - }; - setTimeout(lateFocus, 50); - setTimeout(lateFocus, 300); - - this._ro = new ResizeObserver(() => { - try { - fit.fit(); - if (term.cols < 2 || term.rows < 1) { - term.resize(80, 24); - } - } catch { - /* detached */ - } - }); - this._ro.observe(host); - - this._onWinResize = () => { - try { - fit.fit(); - if (term.cols < 2 || term.rows < 1) { - term.resize(80, 24); - } - } catch { - /* ignore */ - } - }; - window.addEventListener("resize", this._onWinResize); - } - - private _teardownTerminal(): void { - this._inputListeners?.abort(); - this._inputListeners = null; - if (this._ro && this.shadowRoot) { - const host = this.shadowRoot.querySelector(".wc-terminal-host"); - if (host) this._ro.unobserve(host); - this._ro.disconnect(); - } - this._ro = null; - if (this._onWinResize) { - window.removeEventListener("resize", this._onWinResize); - this._onWinResize = null; - } - if (this._term) { - this._term.dispose(); - this._term = null; - this._fit = null; - } - this._started = false; - this._chain = Promise.resolve(); - } -} - -/** - * Same behavior as {@link KeeperShell}; separate class because a custom element - * constructor may only be registered once per registry (two tags ⇒ two classes). - */ -export class WebConsoleElement extends KeeperShell {} - -if (!customElements.get(KEEPER_SHELL_TAG)) { - customElements.define(KEEPER_SHELL_TAG, KeeperShell); -} -if (!customElements.get(WEB_CONSOLE_TAG)) { - customElements.define(WEB_CONSOLE_TAG, WebConsoleElement); -} diff --git a/shellcomponent/src/bufferPolyfill.ts b/shellcomponent/src/bufferPolyfill.ts deleted file mode 100644 index 3fa88e59..00000000 --- a/shellcomponent/src/bufferPolyfill.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Buffer } from "buffer"; - -const g = globalThis as typeof globalThis & { Buffer?: typeof Buffer }; -if (typeof g.Buffer === "undefined") { - g.Buffer = Buffer; -} diff --git a/shellcomponent/src/cli/cliComplete.ts b/shellcomponent/src/cli/cliComplete.ts deleted file mode 100644 index 1595c994..00000000 --- a/shellcomponent/src/cli/cliComplete.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Tab-completion for keeper-shell (SDK command registry). - */ -import { - getCliCommand, - listCliCommandNamesForLoginState, - type KeeperCliHost, -} from "@keeper-security/keeper-sdk-javascript"; - -const HELP_FLAGS = ["--help", "-h"] as const; - -function flagsFor(cmd: string): readonly string[] { - const def = getCliCommand(cmd); - const extra = def?.flagOptions ?? []; - return [...HELP_FLAGS, ...extra]; -} - -export type CliCompleteResult = { - base: string; - candidates: string[]; -}; - -export type CliCompleteOptions = { - loggedIn?: boolean - host?: KeeperCliHost -} - -export function completeCliLine(line: string, options?: CliCompleteOptions): CliCompleteResult { - const loggedIn = options?.host?.getVault().isLoggedIn ?? options?.loggedIn ?? true - const completesNewWord = /\s$/.test(line); - const segments = line.match(/\S+/g) ?? []; - - let words: string[]; - let stub: string; - - if (completesNewWord) { - words = [...segments]; - stub = ""; - } else if (segments.length === 0) { - words = []; - stub = ""; - } else { - words = segments.slice(0, -1); - stub = segments[segments.length - 1] ?? ""; - } - - const lc = (s: string) => s.toLowerCase(); - const stubLc = lc(stub); - - const baseFor = (partialLen: number) => - partialLen > 0 ? line.slice(0, line.length - partialLen) : line; - - if (words.length === 0) { - const top = [...listCliCommandNamesForLoginState(loggedIn)]; - const hits = top.filter((c) => c.startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: hits }; - } - - const cmd = lc(words[0]); - const def = getCliCommand(cmd); - - if (words.length === 1) { - if (stub.startsWith("-")) { - const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: [...hits] }; - } - const subs = def?.subcommands; - if (subs?.length) { - const hits = subs.filter((s) => lc(s).startsWith(stubLc)); - if (hits.length > 0) { - return { base: baseFor(stub.length), candidates: [...hits] }; - } - } - if (completesNewWord || stub.length > 0) { - const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: [...hits] }; - } - } - - if (words.length >= 2 && def?.subcommands) { - const subLc = lc(words[1]); - const knownSub = def.subcommands.some((s) => lc(s) === subLc); - if (knownSub && (stub.startsWith("-") || completesNewWord)) { - const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); - return { base: baseFor(stub.length), candidates: [...hits] }; - } - } - - if (words.length >= 2 && !def?.subcommands && (stub.startsWith("-") || completesNewWord)) { - const hits = flagsFor(cmd).filter((f) => lc(f).startsWith(stubLc)); - if (hits.length > 0) { - return { base: baseFor(stub.length), candidates: [...hits] }; - } - } - - return { base: line, candidates: [] }; -} diff --git a/shellcomponent/src/cli/cliContext.ts b/shellcomponent/src/cli/cliContext.ts deleted file mode 100644 index caeec3db..00000000 --- a/shellcomponent/src/cli/cliContext.ts +++ /dev/null @@ -1,51 +0,0 @@ -export type ShellCliContext = { - /** Optional Keeper host / region (overrides KEEPER_HOST when set). */ - keeperHost?: string; -}; - -let ctx: ShellCliContext = {}; - -export function setShellCliContext(next: ShellCliContext): void { - ctx = { ...next }; -} - -export function getShellKeeperHost(): string | undefined { - const fromCtx = ctx.keeperHost?.trim(); - if (fromCtx) return fromCtx; - if (typeof process !== "undefined" && process.env) { - const h = (process.env.KEEPER_HOST || "").trim(); - if (h) return h; - } - return undefined; -} - -const VITE_ENV_MAP: Record = { - KEEPER_USERNAME: "VITE_KEEPER_USERNAME", - KEEPER_PASSWORD: "VITE_KEEPER_PASSWORD", - KEEPER_SESSION_TOKEN: "VITE_KEEPER_SESSION_TOKEN", - KEEPER_HOST: "VITE_KEEPER_HOST", - REGISTER_DEVICE_TOKEN: "VITE_REGISTER_DEVICE_TOKEN", - REGISTER_DEVICE_PRIVATE_KEY: "VITE_REGISTER_DEVICE_PRIVATE_KEY", - RESTORE_SESSION_JSON: "VITE_RESTORE_SESSION_JSON", -}; - -function readViteEnv(name: string): string | undefined { - try { - const viteKey = VITE_ENV_MAP[name]; - if (!viteKey || typeof import.meta === "undefined" || !import.meta.env) return undefined; - const v = import.meta.env[viteKey as keyof ImportMetaEnv] as string | undefined; - const t = typeof v === "string" ? v.trim() : ""; - return t.length > 0 ? t : undefined; - } catch { - return undefined; - } -} - -/** Node `process.env` in SSR/build; `import.meta.env.VITE_*` in the browser dev bundle. */ -export function envString(name: string): string | undefined { - if (typeof process !== "undefined" && process.env) { - const v = process.env[name]; - if (typeof v === "string" && v.length > 0) return v; - } - return readViteEnv(name); -} diff --git a/shellcomponent/src/cli/cliDispatch.ts b/shellcomponent/src/cli/cliDispatch.ts deleted file mode 100644 index 3976c181..00000000 --- a/shellcomponent/src/cli/cliDispatch.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - dispatchCliLine as sdkDispatchCliLine, - ensureKeeperCliRegistry, -} from "@keeper-security/keeper-sdk-javascript"; -import { shellKeeperCliHost } from "./keeperCliHost.js"; -import type { CliResult } from "./types.js"; - -ensureKeeperCliRegistry(); - -const rawMax = - typeof process !== "undefined" && process.env?.CLI_MAX_LINE_LENGTH - ? Number(process.env.CLI_MAX_LINE_LENGTH) - : NaN; -const MAX_LINE_LENGTH = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 8192; - -/** Shell entry: line-length guard, then SDK dispatch (restore-session uses raw `--from-json` tail + JSON.parse). */ -export async function dispatchCliLine(line: string): Promise { - if (line.length > MAX_LINE_LENGTH) { - return { code: 1, out: "", err: "line too long\n" }; - } - - return sdkDispatchCliLine(line, shellKeeperCliHost); -} diff --git a/shellcomponent/src/cli/inMemoryConfigLoader.ts b/shellcomponent/src/cli/inMemoryConfigLoader.ts deleted file mode 100644 index 5c589f48..00000000 --- a/shellcomponent/src/cli/inMemoryConfigLoader.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ConfigLoader, KeeperJsonConfig } from "@keeper-security/keeper-sdk-javascript"; - -/** - * When `import.meta.env.DEV` is true and all three Vite env vars are set, preloads the same - * flat device shape as `~/.keeper/config.json` so {@link SessionManager.getDeviceConfig} returns - * a token/key before login — keeperapi `loginV3` then skips `registerDevice()`. - * Use the same username you pass to `login` as `VITE_KEEPER_DEV_DEVICE_USER` (case-insensitive match). - */ -function readDevSeededKeeperConfig(): KeeperJsonConfig { - if (import.meta.env?.DEV !== true) return {}; - const user = (import.meta.env.VITE_KEEPER_DEV_DEVICE_USER as string | undefined)?.trim(); - const device_token = (import.meta.env.VITE_KEEPER_DEV_DEVICE_TOKEN as string | undefined)?.trim(); - const private_key = (import.meta.env.VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY as string | undefined)?.trim(); - if (!user || !device_token || !private_key) return {}; - return { - user, - last_login: user, - device_token, - private_key, - }; -} - -/** - * Session/device storage for {@link KeeperVault} inside the browser shell (no `.keeper` on disk). - */ -export class InMemoryConfigLoader implements ConfigLoader { - public readonly configDir = ""; - - private data: KeeperJsonConfig; - - constructor() { - const seed = readDevSeededKeeperConfig(); - this.data = Object.keys(seed).length > 0 ? seed : {}; - } - - async load(): Promise { - return JSON.parse(JSON.stringify(this.data)) as KeeperJsonConfig; - } - - async save(config: KeeperJsonConfig): Promise { - this.data = JSON.parse(JSON.stringify(config)) as KeeperJsonConfig; - } -} diff --git a/shellcomponent/src/cli/keeperCliHost.ts b/shellcomponent/src/cli/keeperCliHost.ts deleted file mode 100644 index 31749689..00000000 --- a/shellcomponent/src/cli/keeperCliHost.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { KeeperCliHost, KeeperCliVault } from "@keeper-security/keeper-sdk-javascript"; -import * as KeeperSdk from "@keeper-security/keeper-sdk-javascript"; -import { envString, getShellKeeperHost } from "./cliContext.js"; -import { InMemoryConfigLoader } from "./inMemoryConfigLoader.js"; - -const { KeeperVault, LogLevel, SessionManager, SdkDefaults } = KeeperSdk; - -type VaultInstance = InstanceType; - -let vault: VaultInstance | null = null; - -function getVault(): VaultInstance { - if (!vault) { - const host = getShellKeeperHost(); - vault = new KeeperVault({ - ...(host ? { host } : {}), - useConsoleAuth: false, - logLevel: LogLevel.WARN, - sessionStorage: new SessionManager(new InMemoryConfigLoader()), - }); - if (import.meta.env?.DEV === true) { - const devDevice = - import.meta.env.VITE_KEEPER_DEV_DEVICE_USER && - import.meta.env.VITE_KEEPER_DEV_DEVICE_TOKEN && - import.meta.env.VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY; - console.info("[keeper-shell dev] KeeperVault created", { - host: host || "(SDK default; US prod host if unset)", - clientVersion: SdkDefaults.CLIENT_VERSION, - useConsoleAuth: false, - logLevel: "WARN", - preseededDeviceFromEnv: Boolean(devDevice), - }); - } - } - return vault; -} - -export function resetShellVault(): void { - vault = null; -} - -function errMsg(e: unknown): string { - return e instanceof Error ? e.message : String(e); -} - -function formatKeeperClientError(context: string, e: unknown): string { - const base = errMsg(e); - const inBrowser = typeof document !== "undefined"; - if (!inBrowser) { - return `${context}: ${base}\n`; - } - const low = base.toLowerCase(); - const fetchish = - low.includes("failed to fetch") || - low.includes("networkerror") || - low.includes("load failed") || - low.includes("network request failed"); - if (!fetchish) { - return `${context}: ${base}\n`; - } - const host = getShellKeeperHost(); - const hostHint = host - ? `\n Region/host in use: ${host} — wrong region can look like a network error; adjust keeper-host or KEEPER_HOST if needed.` - : `\n Region/host: default production (US). EU/AU/CA/JP tenants often need keeper-host on or KEEPER_HOST.`; - return ( - `${context}: ${base}\n` + - ` Browser could not reach Keeper (CORS, network, or wrong region).` + - hostHint + - `\n Dev: run shellcomponent with \`npm run dev\` (same-origin proxy). Prod: set keeper-host on the element or host-page proxy for Keeper API.\n` - ); -} - -function asCliVault(v: VaultInstance): KeeperCliVault { - return { - get isLoggedIn() { - return v.isLoggedIn; - }, - login: (u, p) => v.login(u, p), - loginWithSessionToken: (u, t) => v.loginWithSessionToken(u, t), - logout: () => v.logout(), - sync: () => v.sync(), - getRecords: () => v.getRecords(), - getSharedFolders: () => v.getSharedFolders(), - registerDevice: (dt, pk, o) => v.registerDevice(dt, pk, o), - restoreSession: (input) => v.restoreSession(input), - getSummary: () => v.getSummary(), - findRecord: (uidOrTitle) => v.findRecord(uidOrTitle), - findRecords: (criteria) => v.findRecords(criteria), - getRecordShareInfo: (uid) => v.getRecordShareInfo(uid), - listSharedFolders: (options) => v.listSharedFolders(options), - listFolder: (options) => v.listFolder(options), - tree: (options) => v.tree(options), - changeDirectory: (path) => v.changeDirectory(path), - getCurrentFolderUid: () => v.getCurrentFolderUid(), - getWorkingFolderDisplayName: () => v.getWorkingFolderDisplayName(), - getFolder: (uidOrName, options) => v.getFolder(uidOrName, options), - mkdir: (path, options) => v.mkdir(path, options), - renameFolder: (path, name) => v.renameFolder(path, name), - rmdir: (patterns, options) => v.rmdir(patterns, options), - listTeams: (options) => v.listTeams(options), - viewTeam: (id) => v.viewTeam(id), - listUsers: (options) => v.listUsers(options), - viewUser: (id) => v.viewUser(id), - }; -} - -function isHtmlLike(text: string): boolean { - const head = text.trimStart().slice(0, 32).toLowerCase(); - return head.startsWith(" { - const p = path.trim().replace(/^@/, ""); - - if (/^https?:\/\//i.test(p)) { - const res = await fetch(p); - if (!res.ok) { - throw new Error(`HTTP ${res.status} loading ${p}`); - } - const text = await res.text(); - if (isHtmlLike(text)) { - throw new Error(`URL returned HTML, not JSON: ${p}`); - } - return text; - } - - const isDev = import.meta.env?.DEV === true; - - const tryFetch = async (url: string): Promise => { - const res = await fetch(url); - if (!res.ok) return null; - const ct = res.headers.get("content-type") ?? ""; - if (ct.includes("text/html")) return null; - const text = await res.text(); - if (isHtmlLike(text)) return null; - return text; - }; - - if (isDev && p.startsWith("/")) { - const body = await tryFetch(`/@fs${p}`); - if (body !== null) return body; - throw new Error( - `Could not read ${p}. Run \`npm run dev\` in shellcomponent and ensure the path is in Vite fs.allow.` - ); - } - - if (isDev && (p === "conf.json" || p.endsWith("/conf.json"))) { - const fromDev = await tryFetch("/dev/keeper-session.json"); - if (fromDev !== null) return fromDev; - } - - if (p.startsWith("/") || p.startsWith("./") || p.startsWith("../")) { - throw new Error( - `Cannot read local file path in the browser: ${p}\n` + - ` Use restore-session --from-json with inline JSON, or an https:// URL your app serves.\n` + - (isDev ? ` Dev only: absolute path via Vite (/@fs…) or /dev/keeper-session.json.\n` : "") - ); - } - - const body = await tryFetch(p); - if (body !== null) return body; - - throw new Error( - `Could not load session JSON from ${p}.\n` + - ` Pass inline JSON to --from-json, or a URL that returns application/json.\n` + - (isDev - ? ` Dev: restore-session --from-json /dev/keeper-session.json or an absolute /@fs path.\n` - : ` Prod: host the session file on your origin or paste JSON into --from-json.\n`) - ); -} - -export const shellKeeperCliHost: KeeperCliHost = { - getVault: () => asCliVault(getVault()), - envString, - formatError: formatKeeperClientError, - readTextFile, - getAccountUsername: () => getVault().getAccountUsername(), -}; diff --git a/shellcomponent/src/cli/keeperCommands.ts b/shellcomponent/src/cli/keeperCommands.ts deleted file mode 100644 index dcabc819..00000000 --- a/shellcomponent/src/cli/keeperCommands.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Shell exports for Keeper CLI login helpers (used by UI password transport). - */ -import { - loginWithCredentials as sdkLoginWithCredentials, - loginWithSessionToken as sdkLoginWithSessionToken, - type CliResult, -} from "@keeper-security/keeper-sdk-javascript"; -import { shellKeeperCliHost } from "./keeperCliHost.js"; - -export { resetShellVault } from "./keeperCliHost.js"; - -export async function loginWithCredentials(username: string, password: string): Promise { - return sdkLoginWithCredentials(shellKeeperCliHost, username, password); -} - -export async function loginWithSessionToken( - username: string, - sessionToken: string, - options?: { plainToken?: boolean } -): Promise { - return sdkLoginWithSessionToken(shellKeeperCliHost, username, sessionToken, options); -} - -/** @deprecated Use {@link loginWithSessionToken}. */ -export const loginWithSessionTokenCredentials = loginWithSessionToken; diff --git a/shellcomponent/src/cli/shellBanner.ts b/shellcomponent/src/cli/shellBanner.ts deleted file mode 100644 index 0fd116be..00000000 --- a/shellcomponent/src/cli/shellBanner.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { SdkDefaults } from "@keeper-security/keeper-sdk-javascript"; -import shellPkg from "../../package.json"; - -const ESC = { - reset: "\x1b[0m", - dim: "\x1b[2m", - yellow: "\x1b[93m", - green: "\x1b[32m", - white: "\x1b[97m", -}; - -export type ShellWelcomeOptions = { - cols: number; - loggedIn: boolean; - keeperHost?: string; - shellVersion?: string; - sdkVersion?: string; -}; - -/** Keeper lock + tagline (Commander display.welcome lines 1–7). */ -const KEEPER_ART = [ - " /#############/ /#\\ ", - " /#############/ /###\\ _ __ _______ _______ ______ _______ ______ (R)", - " /#############/ /#####\\ | | / / | ____/ | ____/ | ___ \\ | ____/ | ___ \\ ", - " /######/ \\######\\ | | / / | |____ | |____ | | __| || |____ | | __| | ", - " /######/ \\######\\ | |< < | ___/ | ___/ | |/___/ | ___/ | |/_ / ", - " /######/ \\######\\ | | \\ \\ | |_____ | |_____ | | | |_____ | | \\ \\ ", - " \\######\\ /######/ |_| \\_\\ |_______||_______||_| |_______||_| \\_\\ ", -] as const; - -/** Dual-banner rows (lock prefix + Commander word art), Commander lines 8–12. */ -const DUAL_BANNER_ROWS: ReadonlyArray = [ - [" \\######\\ /######/", " ____ _ "], - [" \\######\\ /######/ ", " / ___|___ _ __ ___ _ __ ___ __ _ _ __ __| | ___ _ __ "], - [" \\#############\\ \\#####/ ", " / / / _ \\| '_ ` _ \\| '_ ` _ \\ / _` | '_ \\ / _` |/ _ \\ '__| "], - [" \\#############\\ \\###/ ", " \\ \\__| (_) | | | | | | | | | | | (_| | | | | (_| | __/ | "], - [" \\#############\\ \\#/ ", " \\_____\\___/|_| |_| |_|_| |_| |_|\\__,_|_| |_|\\__,_|\\___|_| "], -] as const; - -/** Narrow terminal: lock graphic only (leading spaces preserved). */ -const NARROW_LOCK_ART = [ - " /#############/ /#\\ ", - " /#############/ /###\\", - " /#############/ /#####\\", - " /######/ \\######\\", - " /######/ \\######\\", - " /######/ \\######\\", - " \\######\\ /######/", - " \\######\\ /######/", - " \\######\\ /######/ ", - " \\#############\\ \\#####/ ", - " \\#############\\ \\###/ ", - " \\#############\\ \\#/ ", -] as const; - -type BannerTier = "wide" | "narrow" | "minimal"; - -/** Full dual banner needs ~118 cols; below that use lock-only art. */ -const TIER_WIDE_COLS = 118; -const TIER_NARROW_COLS = 48; - -const COMMANDER_VERSION_ALIGN_COL = 93; - -function bannerTier(cols: number): BannerTier { - if (cols >= TIER_WIDE_COLS) return "wide"; - if (cols >= TIER_NARROW_COLS) return "narrow"; - return "minimal"; -} - -/** Truncate at terminal edge only — never trim leading spaces (ASCII art alignment). */ -function fitArtLine(line: string, cols: number): string { - return line.length > cols ? line.slice(0, cols) : line; -} - -/** Word-wrap prose only (not ASCII art). */ -export function wrapPlainText(text: string, cols: number): string[] { - const width = Math.max(cols, 16); - const trimmed = text.trim(); - if (!trimmed) return []; - if (trimmed.length <= width) return [trimmed]; - - const words = trimmed.split(/\s+/); - const lines: string[] = []; - let current = ""; - - for (const word of words) { - if (word.length > width) { - if (current) { - lines.push(current); - current = ""; - } - for (let i = 0; i < word.length; i += width) { - lines.push(word.slice(i, i + width)); - } - continue; - } - const candidate = current ? `${current} ${word}` : word; - if (candidate.length > width) { - lines.push(current); - current = word; - } else { - current = candidate; - } - } - if (current) lines.push(current); - return lines; -} - -function yellow(text: string): string { - return `${ESC.yellow}${text}${ESC.reset}`; -} - -function white(text: string): string { - return `${ESC.white}${text}${ESC.reset}`; -} - -function dim(text: string): string { - return `${ESC.dim}${text}${ESC.reset}`; -} - -/** Commander display.welcome dual-line logic (truncate, never reflow). */ -function pushDualRow(out: string[], left: string, right: string, cols: number): void { - let yellowLine = left; - let whiteLine = right; - if (yellowLine.length > cols) { - yellowLine = yellowLine.slice(0, cols); - } - if (yellowLine.length + whiteLine.length > cols) { - if (yellowLine.length < cols) { - whiteLine = whiteLine.slice(0, cols - yellowLine.length); - } else { - whiteLine = ""; - } - } - if (whiteLine) { - out.push(`${ESC.yellow}${yellowLine}${ESC.white}${whiteLine}${ESC.reset}`); - } else { - out.push(yellow(yellowLine)); - } -} - -/** Responsive Commander banner — preserves fixed-width ASCII alignment. */ -export function bannerLines(cols: number): string[] { - const width = Math.max(cols, 24); - const tier = bannerTier(width); - const out: string[] = []; - - if (tier === "minimal") { - out.push(yellow("Keeper Commander Shell")); - return out; - } - - if (tier === "narrow") { - for (const line of NARROW_LOCK_ART) { - out.push(yellow(fitArtLine(line, width))); - } - out.push(white(fitArtLine("Commander", width))); - return out; - } - - for (const line of KEEPER_ART) { - out.push(yellow(fitArtLine(line, width))); - } - for (const [left, right] of DUAL_BANNER_ROWS) { - pushDualRow(out, left, right, width); - } - return out; -} - -function versionLine(shellVersion: string, sdkVersion: string, cols: number): string { - const label = `v${shellVersion} · SDK ${sdkVersion}`; - if (cols < 48) return dim(label); - const alignTo = Math.min(COMMANDER_VERSION_ALIGN_COL, cols); - const pad = Math.max(0, alignTo - label.length); - return `${ESC.dim}${" ".repeat(pad)}${label}${ESC.reset}`; -} - -function notLoggedInMessages(cols: number, keeperHost?: string): string[] { - const out: string[] = []; - out.push("You are not logged in."); - - if (keeperHost) { - out.push(...wrapPlainText(`Region: ${keeperHost}`, cols)); - } - - if (cols >= 100) { - out.push( - `Type ${ESC.green}login ${ESC.reset} to authenticate, ` + - `${ESC.green}restore-session${ESC.reset} to resume, or ` + - `${ESC.green}register-device${ESC.reset} for token login.` - ); - } else { - out.push(`Type ${ESC.green}login ${ESC.reset} to authenticate.`); - out.push( - `${ESC.green}restore-session${ESC.reset} to resume, or ` + - `${ESC.green}register-device${ESC.reset} for token login.` - ); - } - out.push(`Type ${ESC.green}help${ESC.reset} or ${ESC.green}?${ESC.reset} for sign-in commands.`); - return out; -} - -function loggedInMessages(cols: number): string[] { - const out: string[] = [ - `Type ${ESC.green}help${ESC.reset} or ${ESC.green}?${ESC.reset} for available commands.`, - ]; - for (const row of wrapPlainText( - "Vault shell — get, ls, cd, tree, search, sync-down (see COMMAND --help).", - cols - )) { - out.push(dim(row)); - } - return out; -} - -/** Commander-style startup banner + session hint (browser shell). */ -export function buildShellWelcomeLines(options: ShellWelcomeOptions): string[] { - const cols = Math.max(options.cols, 24); - const shellVersion = options.shellVersion ?? shellPkg.version; - const sdkVersion = options.sdkVersion ?? SdkDefaults.CLIENT_VERSION; - const lines: string[] = [""]; - - lines.push(...bannerLines(cols)); - lines.push(versionLine(shellVersion, sdkVersion, cols)); - lines.push(""); - - if (!options.loggedIn) { - lines.push(...notLoggedInMessages(cols, options.keeperHost)); - } else { - lines.push(...loggedInMessages(cols)); - } - - lines.push(""); - return lines; -} - -export function writeShellWelcome( - write: (line: string) => void, - options: ShellWelcomeOptions -): void { - for (const line of buildShellWelcomeLines(options)) { - write(line); - } -} diff --git a/shellcomponent/src/cli/types.ts b/shellcomponent/src/cli/types.ts deleted file mode 100644 index abba04bf..00000000 --- a/shellcomponent/src/cli/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { CliResult } from "@keeper-security/keeper-sdk-javascript"; diff --git a/shellcomponent/src/dev-bootstrap.ts b/shellcomponent/src/dev-bootstrap.ts deleted file mode 100644 index 3a302d1b..00000000 --- a/shellcomponent/src/dev-bootstrap.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Dev page entry only. Dynamic-imports the shell so load failures surface in the page - * (static `import "./KeeperShell.js"` would fail the whole module with no UI feedback). - */ -import "./bufferPolyfill.js"; -import { installKeeperSameOriginProxy } from "./dev/installKeeperSameOriginProxy.js"; - -installKeeperSameOriginProxy(); - -function showShellLoadError(err: unknown): void { - const msg = err instanceof Error ? `${err.message}\n\n${err.stack ?? ""}` : String(err); - document.querySelectorAll("web-console, keeper-shell").forEach((node) => { - const el = node as HTMLElement; - while (el.firstChild) el.removeChild(el.firstChild); - const pre = document.createElement("pre"); - pre.style.cssText = - "display:block;margin:0;padding:16px;background:#2a0a0a;color:#fecaca;border:2px solid #991b1b;border-radius:8px;white-space:pre-wrap;font:13px/1.45 ui-monospace,monospace;"; - pre.textContent = `Keeper shell failed to load:\n\n${msg}\n`; - el.appendChild(pre); - el.style.display = "block"; - el.style.width = "100%"; - el.style.minHeight = "16rem"; - el.style.boxSizing = "border-box"; - }); -} - -void (async () => { - try { - await import("./KeeperShell.js"); - } catch (err: unknown) { - showShellLoadError(err); - console.error("[keeper-shell] failed to load", err); - } -})(); diff --git a/shellcomponent/src/dev/installKeeperSameOriginProxy.ts b/shellcomponent/src/dev/installKeeperSameOriginProxy.ts deleted file mode 100644 index 1e758a4e..00000000 --- a/shellcomponent/src/dev/installKeeperSameOriginProxy.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - isAllowedKeeperProxyHost, - KEEPER_REST_PROXY_PREFIX, - KEEPER_WSS_PROXY_PREFIX, -} from "./keeperProxyHosts.js"; - -function shouldProxyUrl(url: URL): boolean { - return isAllowedKeeperProxyHost(url.hostname); -} - -function rewriteKeeperUrl(url: string): string { - try { - const u = new URL(url, typeof window !== "undefined" ? window.location.href : undefined); - if (!shouldProxyUrl(u)) return url; - - const ws = u.protocol === "wss:" || u.protocol === "ws:"; - const prefix = ws ? KEEPER_WSS_PROXY_PREFIX : KEEPER_REST_PROXY_PREFIX; - const proto = - window.location.protocol === "https:" - ? ws - ? "wss:" - : "https:" - : ws - ? "ws:" - : "http:"; - - return `${proto}//${window.location.host}${prefix}${u.host}${u.pathname}${u.search}`; - } catch { - return url; - } -} - -/** Dev: rewrite Keeper fetch/WebSocket URLs to the same-origin Vite proxy prefixes. */ -export function installKeeperSameOriginProxy(): void { - if (typeof window === "undefined") return; - - const nativeFetch = window.fetch.bind(window); - window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { - if (typeof input === "string") { - return nativeFetch(rewriteKeeperUrl(input), init); - } - if (input instanceof Request) { - const next = rewriteKeeperUrl(input.url); - return next === input.url - ? nativeFetch(input, init) - : nativeFetch(new Request(next, input), init); - } - return nativeFetch(input, init); - }) as typeof fetch; - - const NativeWebSocket = window.WebSocket; - window.WebSocket = new Proxy(NativeWebSocket, { - construct(_target, args: [string | URL, (string | string[])?]) { - const [url, protocols] = args; - return new NativeWebSocket(rewriteKeeperUrl(String(url)), protocols); - }, - }) as typeof WebSocket; - - if (import.meta.env?.DEV === true) { - console.info( - "[keeper-shell] Same-origin dev proxy enabled:", - KEEPER_REST_PROXY_PREFIX, - KEEPER_WSS_PROXY_PREFIX - ); - } -} diff --git a/shellcomponent/src/dev/keeperProxyHosts.ts b/shellcomponent/src/dev/keeperProxyHosts.ts deleted file mode 100644 index 38ba2d8c..00000000 --- a/shellcomponent/src/dev/keeperProxyHosts.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** Allowlist of Keeper infra hosts the dev proxy may forward to. */ -export function isAllowedKeeperProxyHost(host: string): boolean { - if (!host || host.includes("..") || host.includes("/")) return false; - if (/^push\.services\./.test(host)) { - return isAllowedKeeperProxyHost(host.slice("push.services.".length)); - } - if (/^connect\./.test(host)) { - const rest = host.slice("connect.".length); - return ( - rest === "keepersecurity.us" || - /^[a-z0-9.-]+\.keepersecurity\.(com|eu|us)$/.test(rest) || - rest === "keepersecurity.com" || - rest === "keepersecurity.eu" - ); - } - return /^([a-z0-9.-]+\.)?keepersecurity\.(com|eu|us)$/.test(host); -} - -export const KEEPER_REST_PROXY_PREFIX = "/__keeper/"; -export const KEEPER_WSS_PROXY_PREFIX = "/__keeper-wss/"; diff --git a/shellcomponent/src/index.ts b/shellcomponent/src/index.ts deleted file mode 100644 index f864268f..00000000 --- a/shellcomponent/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import "./bufferPolyfill.js"; -/** Registers `` and ``. */ -import "./KeeperShell.js"; - -export { - KeeperShell, - WebConsoleElement, - WebConsoleElement as WebConsole, - KEEPER_SHELL_TAG, - WEB_CONSOLE_TAG, -} from "./KeeperShell.js"; -export { dispatchCliLine } from "./cli/cliDispatch.js"; -export { completeCliLine } from "./cli/cliComplete.js"; -export type { CliResult } from "./cli/types.js"; -export type { ShellCliContext } from "./cli/cliContext.js"; -export { setShellCliContext } from "./cli/cliContext.js"; -export { - resetShellVault, - loginWithCredentials, - loginWithSessionToken, -} from "./cli/keeperCommands.js"; diff --git a/shellcomponent/src/shims/fs-empty.ts b/shellcomponent/src/shims/fs-empty.ts deleted file mode 100644 index ed2ab6d0..00000000 --- a/shellcomponent/src/shims/fs-empty.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** Browser stub: KeeperSdk SessionManager pulls `fs` for FileConfigLoader; embedded shell uses in-memory config only. */ -const notAvailable = (): never => { - throw new Error("Node fs is not available in keeper-shell (use in-memory session storage)."); -}; - -export default { - promises: { - readFile: notAvailable, - writeFile: notAvailable, - mkdir: notAvailable, - access: notAvailable, - }, -}; diff --git a/shellcomponent/src/shims/fs-promises-empty.ts b/shellcomponent/src/shims/fs-promises-empty.ts deleted file mode 100644 index 4e750185..00000000 --- a/shellcomponent/src/shims/fs-promises-empty.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Browser stub for `import fs from "fs/promises"` (KeeperSdk SessionManager / FileConfigLoader). - * The embedded shell uses {@link InMemoryConfigLoader} only; these paths are not used at runtime. - */ -const notAvailable = (): never => { - throw new Error("fs/promises is not available in keeper-shell."); -}; - -const stub = { - readFile: notAvailable, - writeFile: notAvailable, - mkdir: notAvailable, - access: notAvailable, -}; - -export default stub; - -export const readFile = notAvailable; -export const writeFile = notAvailable; -export const mkdir = notAvailable; -export const access = notAvailable; diff --git a/shellcomponent/src/shims/os-homedir.ts b/shellcomponent/src/shims/os-homedir.ts deleted file mode 100644 index 38f82184..00000000 --- a/shellcomponent/src/shims/os-homedir.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** Minimal `os` stub for bundles that never instantiate FileConfigLoader with default paths. */ -export function homedir(): string { - return "/"; -} - -export default { homedir }; diff --git a/shellcomponent/src/vite-env.d.ts b/shellcomponent/src/vite-env.d.ts deleted file mode 100644 index 8305b480..00000000 --- a/shellcomponent/src/vite-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -interface ImportMetaEnv { - readonly VITE_KEEPER_DEV_DEVICE_USER?: string; - readonly VITE_KEEPER_DEV_DEVICE_TOKEN?: string; - readonly VITE_KEEPER_DEV_DEVICE_PRIVATE_KEY?: string; - readonly VITE_KEEPER_USERNAME?: string; - readonly VITE_KEEPER_PASSWORD?: string; - readonly VITE_KEEPER_SESSION_TOKEN?: string; - readonly VITE_KEEPER_HOST?: string; - readonly VITE_REGISTER_DEVICE_TOKEN?: string; - readonly VITE_REGISTER_DEVICE_PRIVATE_KEY?: string; - readonly VITE_RESTORE_SESSION_JSON?: string; -} diff --git a/shellcomponent/tsconfig.json b/shellcomponent/tsconfig.json deleted file mode 100644 index c088d75a..00000000 --- a/shellcomponent/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["DOM", "ESNext"], - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "isolatedModules": true, - "resolveJsonModule": true, - "types": ["node"], - "baseUrl": ".", - "paths": { - "@keeper-security/keeper-sdk-javascript": ["../KeeperSdk/src/browser.ts"] - } - }, - "include": ["src/**/*.ts", "vite.config.ts"] -} diff --git a/shellcomponent/vite.config.ts b/shellcomponent/vite.config.ts deleted file mode 100644 index eb651df7..00000000 --- a/shellcomponent/vite.config.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { readFileSync, existsSync } from "node:fs"; -import { defineConfig, type Plugin } from "vite"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { keeperSameOriginProxyPlugin } from "./vite/keeperSameOriginProxyPlugin.js"; - -const __dirname = fileURLToPath(new URL(".", import.meta.url)); -const repoConfJson = resolve(__dirname, "../conf.json"); - -/** Dev-only: load repo-root conf.json without typing a full /@fs path in the shell. */ -function keeperDevSessionConfPlugin(): Plugin { - return { - name: "keeper-dev-session-conf", - configureServer(server) { - server.middlewares.use("/dev/keeper-session.json", (_req, res, next) => { - try { - if (!existsSync(repoConfJson)) { - res.statusCode = 404; - res.end("conf.json not found at repo root"); - return; - } - res.setHeader("Content-Type", "application/json"); - res.end(readFileSync(repoConfJson)); - } catch (e) { - next(e); - } - }); - }, - }; -} - -export default defineConfig(({ command, mode }) => { - const isServe = command === "serve"; - const isProd = mode === "production"; - - return { - root: ".", - plugins: isServe ? [keeperDevSessionConfPlugin(), keeperSameOriginProxyPlugin()] : [], - resolve: { - alias: { - // Bundle from KeeperSdk sources (CJS dist/browser.js breaks Rollup named exports). - "@keeper-security/keeper-sdk-javascript": resolve( - __dirname, - "../KeeperSdk/src/browser.ts" - ), - "fs/promises": resolve(__dirname, "src/shims/fs-promises-empty.ts"), - fs: resolve(__dirname, "src/shims/fs-empty.ts"), - path: resolve(__dirname, "node_modules/path-browserify/index.js"), - os: resolve(__dirname, "src/shims/os-homedir.ts"), - buffer: "buffer/", - }, - }, - define: { - global: "globalThis", - }, - optimizeDeps: { - include: [ - "@keeper-security/keeper-sdk-javascript", - "@keeper-security/keeperapi", - "@xterm/xterm", - "@xterm/addon-fit", - "buffer", - "protobufjs", - ], - esbuildOptions: { - define: { - global: "globalThis", - }, - }, - }, - build: { - target: "es2017", - sourcemap: isProd ? false : true, - commonjsOptions: { - transformMixedEsModules: true, - }, - lib: { - entry: resolve(__dirname, "src/index.ts"), - name: "KeeperShell", - formats: ["es", "umd"], - fileName: (format) => - format === "umd" ? "keeper-shell.umd.cjs" : "keeper-shell.es.js", - }, - rollupOptions: { - output: { - exports: "named", - assetFileNames: "keeper-shell.[ext]", - }, - }, - }, - server: { - port: 5175, - fs: { - allow: [__dirname, resolve(__dirname, "..")], - }, - }, - }; -}); diff --git a/shellcomponent/vite/keeperSameOriginProxyPlugin.ts b/shellcomponent/vite/keeperSameOriginProxyPlugin.ts deleted file mode 100644 index 73fe160b..00000000 --- a/shellcomponent/vite/keeperSameOriginProxyPlugin.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Plugin } from "vite"; -import httpProxy from "http-proxy"; -import { isAllowedKeeperProxyHost } from "../src/dev/keeperProxyHosts.js"; - -/** Dev: proxy `/__keeper//…` (HTTPS) and `/__keeper-wss//…` (WSS) to Keeper. */ -export function keeperSameOriginProxyPlugin(): Plugin { - const proxy = httpProxy.createProxyServer({ - changeOrigin: true, - secure: true, - ws: true, - }); - - return { - name: "keeper-same-origin-proxy", - configureServer(server) { - server.middlewares.use((req, res, next) => { - const raw = req.url ?? ""; - const match = raw.match(/^\/__keeper\/([^/]+)(\/.*)?$/); - if (!match) { - next(); - return; - } - const targetHost = match[1]; - if (!isAllowedKeeperProxyHost(targetHost)) { - res.statusCode = 403; - res.end("Forbidden keeper proxy host"); - return; - } - req.url = match[2] ?? "/"; - proxy.web( - req, - res, - { target: `https://${targetHost}` }, - (err) => { - if (err) next(err); - } - ); - }); - - server.httpServer?.on("upgrade", (req, socket, head) => { - const raw = req.url ?? ""; - const match = raw.match(/^\/__keeper-wss\/([^/]+)(\/.*)?$/); - if (!match) return; - const targetHost = match[1]; - if (!isAllowedKeeperProxyHost(targetHost)) { - socket.destroy(); - return; - } - req.url = match[2] ?? "/"; - proxy.ws(req, socket, head, { target: `wss://${targetHost}` }); - }); - }, - }; -} From ae52d04cd8c8c60ed21f4850b9f033043c07294e Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 15 Jun 2026 09:13:18 +0530 Subject: [PATCH 20/23] Removed cli changes --- KeeperSdk/src/cli/access.ts | 31 --- KeeperSdk/src/cli/builtinCommands.ts | 47 ---- KeeperSdk/src/cli/commandHelpers.ts | 24 -- KeeperSdk/src/cli/commander/get.ts | 41 --- KeeperSdk/src/cli/commander/getCore.ts | 153 ----------- KeeperSdk/src/cli/commander/index.ts | 4 - KeeperSdk/src/cli/commander/misc.ts | 266 ------------------- KeeperSdk/src/cli/commander/nav.ts | 234 ---------------- KeeperSdk/src/cli/commands/help.ts | 76 ------ KeeperSdk/src/cli/commands/login.ts | 178 ------------- KeeperSdk/src/cli/commands/logout.ts | 39 --- KeeperSdk/src/cli/commands/registerDevice.ts | 90 ------- KeeperSdk/src/cli/commands/restoreSession.ts | 211 --------------- KeeperSdk/src/cli/commands/sync.ts | 61 ----- KeeperSdk/src/cli/commands/users.ts | 115 -------- KeeperSdk/src/cli/commands/vault.ts | 56 ---- KeeperSdk/src/cli/dispatch.ts | 52 ---- KeeperSdk/src/cli/help.ts | 82 ------ KeeperSdk/src/cli/index.ts | 112 -------- KeeperSdk/src/cli/jsonArg.ts | 30 --- KeeperSdk/src/cli/parse.ts | 173 ------------ KeeperSdk/src/cli/parser.ts | 172 ------------ KeeperSdk/src/cli/prompt.ts | 12 - KeeperSdk/src/cli/registry.ts | 55 ---- KeeperSdk/src/cli/table.ts | 8 - KeeperSdk/src/cli/types.ts | 99 ------- KeeperSdk/src/cli/utils.ts | 18 -- KeeperSdk/src/cli/vaultSurface.ts | 18 -- 28 files changed, 2457 deletions(-) delete mode 100644 KeeperSdk/src/cli/access.ts delete mode 100644 KeeperSdk/src/cli/builtinCommands.ts delete mode 100644 KeeperSdk/src/cli/commandHelpers.ts delete mode 100644 KeeperSdk/src/cli/commander/get.ts delete mode 100644 KeeperSdk/src/cli/commander/getCore.ts delete mode 100644 KeeperSdk/src/cli/commander/index.ts delete mode 100644 KeeperSdk/src/cli/commander/misc.ts delete mode 100644 KeeperSdk/src/cli/commander/nav.ts delete mode 100644 KeeperSdk/src/cli/commands/help.ts delete mode 100644 KeeperSdk/src/cli/commands/login.ts delete mode 100644 KeeperSdk/src/cli/commands/logout.ts delete mode 100644 KeeperSdk/src/cli/commands/registerDevice.ts delete mode 100644 KeeperSdk/src/cli/commands/restoreSession.ts delete mode 100644 KeeperSdk/src/cli/commands/sync.ts delete mode 100644 KeeperSdk/src/cli/commands/users.ts delete mode 100644 KeeperSdk/src/cli/commands/vault.ts delete mode 100644 KeeperSdk/src/cli/dispatch.ts delete mode 100644 KeeperSdk/src/cli/help.ts delete mode 100644 KeeperSdk/src/cli/index.ts delete mode 100644 KeeperSdk/src/cli/jsonArg.ts delete mode 100644 KeeperSdk/src/cli/parse.ts delete mode 100644 KeeperSdk/src/cli/parser.ts delete mode 100644 KeeperSdk/src/cli/prompt.ts delete mode 100644 KeeperSdk/src/cli/registry.ts delete mode 100644 KeeperSdk/src/cli/table.ts delete mode 100644 KeeperSdk/src/cli/types.ts delete mode 100644 KeeperSdk/src/cli/utils.ts delete mode 100644 KeeperSdk/src/cli/vaultSurface.ts diff --git a/KeeperSdk/src/cli/access.ts b/KeeperSdk/src/cli/access.ts deleted file mode 100644 index 1f6f1ebf..00000000 --- a/KeeperSdk/src/cli/access.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { CliCommandDefinition } from './types' -import { listCliCommands, resolveCliCommandName } from './registry' - -/** Commands available before a vault session exists. */ -export const AUTH_CLI_COMMAND_NAMES = new Set([ - 'help', - 'login', - 'restore-session', - 'register-device', -]) - -export function isAuthCliCommand(name: string): boolean { - const resolved = resolveCliCommandName(name) - return resolved != null && AUTH_CLI_COMMAND_NAMES.has(resolved) -} - -export function filterCliCommandsForLoginState( - commands: readonly CliCommandDefinition[], - loggedIn: boolean -): CliCommandDefinition[] { - if (loggedIn) return [...commands] - return commands.filter((c) => AUTH_CLI_COMMAND_NAMES.has(c.name)) -} - -export function listCliCommandsForLoginState(loggedIn: boolean): CliCommandDefinition[] { - return filterCliCommandsForLoginState(listCliCommands(), loggedIn) -} - -export function listCliCommandNamesForLoginState(loggedIn: boolean): readonly string[] { - return listCliCommandsForLoginState(loggedIn).map((c) => c.name) -} diff --git a/KeeperSdk/src/cli/builtinCommands.ts b/KeeperSdk/src/cli/builtinCommands.ts deleted file mode 100644 index aed2457d..00000000 --- a/KeeperSdk/src/cli/builtinCommands.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CliCommandDefinition } from './types' -import { registerCliCommand } from './registry' -import { helpCommand } from './commands/help' -import { loginCommand } from './commands/login' -import { logoutCommand } from './commands/logout' -import { registerDeviceCommand } from './commands/registerDevice' -import { restoreSessionCommand } from './commands/restoreSession' -import { syncCommand } from './commands/sync' -import { usersCommand } from './commands/users' -import { vaultCommand } from './commands/vault' -import { getCommand } from './commander/get' -import { cdCommand, lsCommand, mkdirCommand, treeCommand } from './commander/nav' -import { - listCommand, - listSfCommand, - listTeamCommand, - searchCommand, - whoamiCommand, -} from './commander/misc' - -/** Built-in CLI commands (Keeper Commander-style vault shell). */ -export const BUILTIN_CLI_COMMANDS: readonly CliCommandDefinition[] = [ - helpCommand, - loginCommand, - registerDeviceCommand, - restoreSessionCommand, - syncCommand, - vaultCommand, - getCommand, - listCommand, - lsCommand, - cdCommand, - treeCommand, - mkdirCommand, - searchCommand, - listSfCommand, - listTeamCommand, - whoamiCommand, - usersCommand, - logoutCommand, -] - -export function registerBuiltinCliCommands(): void { - for (const def of BUILTIN_CLI_COMMANDS) { - registerCliCommand(def) - } -} diff --git a/KeeperSdk/src/cli/commandHelpers.ts b/KeeperSdk/src/cli/commandHelpers.ts deleted file mode 100644 index e4fa6652..00000000 --- a/KeeperSdk/src/cli/commandHelpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CliResult, KeeperCliHost, KeeperCliVault } from './types' -import { ensureLoggedIn } from './commands/login' - -export async function ensureSession(host: KeeperCliHost): Promise { - const v = host.getVault() - if (v.isLoggedIn) return null - const r = await ensureLoggedIn(host) - return r.code === 0 ? null : r -} - -export function ensureCapability( - v: KeeperCliVault, - name: K, - context: string -): CliResult | null { - if (typeof v[name] !== 'function') { - return { - code: 1, - out: '', - err: `${context}: this host does not expose KeeperCliVault.${String(name)}.\n`, - } - } - return null -} diff --git a/KeeperSdk/src/cli/commander/get.ts b/KeeperSdk/src/cli/commander/get.ts deleted file mode 100644 index e31a19bf..00000000 --- a/KeeperSdk/src/cli/commander/get.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CliCommandDefinition } from '../types' -import { wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { executeGet } from './getCore' - -export const getCommand: CliCommandDefinition = { - name: 'get', - order: 10, - aliases: ['g'], - description: 'Get details of a record, folder, or team by UID or title.', - usage: 'get [--format {detail,json,password,fields}] [--unmask]', - flagOptions: ['--format', '--unmask', '--detail', '--json'], - help: { - title: 'get — record/folder/team details (Keeper Commander)', - synopsis: 'usage: get [--unmask] [--format {detail,json,password,fields}] uid', - description: - ' Resolves a vault object by UID or title. Records support all output formats; folders and teams support detail/json.', - arguments: ' uid Record, folder, or team UID or title.', - options: ` --format {detail,json,password,fields} - detail (default): human-readable output. - json: JSON object. - password: password field only (records). - fields: JSON array of {name, value} (records). - --unmask Show sensitive field values (records). - --help, -h Show this help.`, - examples: ` get "Amazon" - get AbCdEf123456 --format json --unmask - get MyFolderUid --format json`, - seeAlso: ' ls, search, record-update', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(getCommand), err: '' } - } - try { - return await executeGet(host, parsed, 'get') - } catch (e) { - return { code: 1, out: '', err: host.formatError('get', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commander/getCore.ts b/KeeperSdk/src/cli/commander/getCore.ts deleted file mode 100644 index ceec4a65..00000000 --- a/KeeperSdk/src/cli/commander/getCore.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { DRecord } from '@keeper-security/keeperapi' -import { - formatRecord, - formatRecordFields, - getRecordPassword, - getRecordTitle, -} from '../../records/RecordUtils' -import { formatTeamView, teamViewTable } from '../../teams/viewTeam' -import type { CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt } from '../parse' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { GetFolderFormat } from '../../folders/getFolder' - -export type GetOutputFormat = 'detail' | 'json' | 'password' | 'fields' - -export function resolveGetFormat(parsed: ParsedCli): GetOutputFormat { - const raw = getOpt(parsed.opts, 'format')?.toLowerCase() - if (raw === 'json' || hasOpt(parsed.opts, 'json')) return 'json' - if (raw === 'password') return 'password' - if (raw === 'fields') return 'fields' - if (raw === 'detail') return 'detail' - return hasOpt(parsed.opts, 'detail') ? 'detail' : 'detail' -} - -export function resolveGetUnmask(parsed: ParsedCli): boolean { - return hasOpt(parsed.opts, 'unmask') -} - -export function getGetTarget(parsed: ParsedCli): string | undefined { - return parsed.positional[0]?.trim() || undefined -} - -async function outputRecord( - host: KeeperCliHost, - record: DRecord, - fmt: GetOutputFormat, - unmask: boolean, - cmd: string -): Promise { - if (fmt === 'password') { - const pw = getRecordPassword(record) - return { code: 0, out: pw ? `${pw}\n` : '', err: pw ? '' : `${cmd}: record has no password field\n` } - } - if (fmt === 'fields') { - return { code: 0, out: JSON.stringify(formatRecordFields(record, unmask), null, 2) + '\n', err: '' } - } - if (fmt === 'json') { - return { code: 0, out: JSON.stringify(record, null, 2) + '\n', err: '' } - } - return { code: 0, out: formatRecord(record, { showDetails: true, unmask }) + '\n', err: '' } -} - -async function tryGetFolder( - host: KeeperCliHost, - target: string, - fmt: GetOutputFormat, - cmd: string -): Promise { - const v = host.getVault() - if (!v.getFolder) return null - try { - const res = await v.getFolder(target, { - format: fmt === 'json' ? GetFolderFormat.JSON : GetFolderFormat.Detail, - }) - if (fmt === 'json') { - const json = (res as { json?: Record }).json ?? res - return { code: 0, out: JSON.stringify(json, null, 2) + '\n', err: '' } - } - const name = 'name' in res ? res.name : target - const uid = - 'folder_uid' in res - ? res.folder_uid - : 'shared_folder_uid' in res - ? res.shared_folder_uid - : target - return { code: 0, out: `${name}\t${uid}\n`, err: '' } - } catch { - return null - } -} - -async function tryGetTeam( - host: KeeperCliHost, - target: string, - fmt: GetOutputFormat, - cmd: string -): Promise { - const v = host.getVault() - if (!v.viewTeam) return null - try { - const view = await v.viewTeam(target) - if (fmt === 'json') { - return { code: 0, out: JSON.stringify(view, null, 2) + '\n', err: '' } - } - if (fmt === 'password' || fmt === 'fields') { - return { code: 1, out: '', err: `${cmd}: --format ${fmt} applies to records only\n` } - } - const formatted = formatTeamView(view, { verbose: true }) - return { code: 0, out: teamViewTable(formatted) + '\n', err: '' } - } catch { - return null - } -} - -async function tryGetSharedFolderByUid( - host: KeeperCliHost, - target: string, - fmt: GetOutputFormat, - cmd: string -): Promise { - const v = host.getVault() - const hit = v.getSharedFolders().find((sf) => sf.uid === target) - if (!hit) return null - if (fmt === 'json') { - return { code: 0, out: JSON.stringify(hit, null, 2) + '\n', err: '' } - } - if (fmt === 'password' || fmt === 'fields') { - return { code: 1, out: '', err: `${cmd}: --format ${fmt} applies to records only\n` } - } - return { code: 0, out: `${hit.name ?? '(unnamed)'}\t${hit.uid}\n`, err: '' } -} - -/** Commander-style `get` (record, folder, team, or shared folder by UID/title). */ -export async function executeGet(host: KeeperCliHost, parsed: ParsedCli, cmd = 'get'): Promise { - const target = getGetTarget(parsed) - if (!target) { - return { code: 1, out: '', err: `${cmd}: UID parameter is required\n` } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const fmt = resolveGetFormat(parsed) - const unmask = resolveGetUnmask(parsed) - - await v.sync() - - const sf = await tryGetSharedFolderByUid(host, target, fmt, cmd) - if (sf) return sf - - const folder = await tryGetFolder(host, target, fmt, cmd) - if (folder) return folder - - const team = await tryGetTeam(host, target, fmt, cmd) - if (team) return team - - const cap = ensureCapability(v, 'findRecord', cmd) - if (cap) return cap - const record = v.findRecord!(target) - if (!record) { - return { code: 1, out: '', err: `${cmd}: cannot find any object matching "${target}"\n` } - } - return outputRecord(host, record, fmt, unmask, cmd) -} diff --git a/KeeperSdk/src/cli/commander/index.ts b/KeeperSdk/src/cli/commander/index.ts deleted file mode 100644 index fb25cce5..00000000 --- a/KeeperSdk/src/cli/commander/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { getCommand } from './get' -export { executeGet } from './getCore' -export { lsCommand, cdCommand, treeCommand, mkdirCommand } from './nav' -export { listCommand, searchCommand, listSfCommand, listTeamCommand, whoamiCommand } from './misc' diff --git a/KeeperSdk/src/cli/commander/misc.ts b/KeeperSdk/src/cli/commander/misc.ts deleted file mode 100644 index ee7babae..00000000 --- a/KeeperSdk/src/cli/commander/misc.ts +++ /dev/null @@ -1,266 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { formatTable } from '../table' -import { getRecordTitle } from '../../records/RecordUtils' -import { renderRecordsListTable } from '../../records/listRecordsTable' -import { recordUid } from '../utils' -import { formatSharedFoldersTable, renderSharedFoldersAsciiTable } from '../../sharedFolders/listSharedFolders' -import { formatTeamsTable, renderTeamsAsciiTable, type ListTeamSort } from '../../teams/listTeams' - -async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - await v.sync() - const records = v.getRecords() - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(records, null, 2) + '\n', err: '' } - } - if (records.length === 0) { - return { code: 0, out: '(no records)\n', err: '' } - } - const verbose = hasOpt(parsed.opts, 'verbose') || hasOpt(parsed.opts, 'v') - const out = renderRecordsListTable(records, { verbose }) + '\n' - return { code: 0, out, err: '' } -} - -async function runSearch(host: KeeperCliHost, parsed: ParsedCli): Promise { - const pattern = parsed.positional.join(' ') || getOpt(parsed.opts, 'pattern') - if (!pattern?.trim()) { - return { code: 1, out: '', err: 'search: missing search terms. Usage: search \n' } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'findRecords', 'search') - if (cap) return cap - await v.sync() - const matches = v.findRecords!(pattern) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(matches, null, 2) + '\n', err: '' } - } - if (matches.length === 0) { - return { code: 0, out: `(no records matched "${pattern}")\n`, err: '' } - } - const rows = matches.map((rec) => [recordUid(rec), getRecordTitle(rec)]) - return { code: 0, out: formatTable(['record_uid', 'title'], rows), err: '' } -} - -async function runListSf(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listSharedFolders', 'list-sf') - if (cap) return cap - await v.sync() - const pattern = parsed.positional[0] ?? getOpt(parsed.opts, 'pattern') ?? null - const verbose = hasOpt(parsed.opts, 'verbose') || hasOpt(parsed.opts, 'v') - const rows = v.listSharedFolders!({ pattern, verbose, includeDetails: verbose }) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } - } - if (rows.length === 0) { - return { - code: 0, - out: pattern ? `(no shared folders matched "${pattern}")\n` : '(no shared folders)\n', - err: '', - } - } - const table = formatSharedFoldersTable(rows, { verbose }) - return { code: 0, out: renderSharedFoldersAsciiTable(table) + '\n', err: '' } -} - -const LIST_TEAM_SORTS = new Set(['company', 'team_uid', 'name']) - -async function runListTeam(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listTeams', 'list-team') - if (cap) return cap - await v.sync() - const pattern = parsed.positional[0] ?? getOpt(parsed.opts, 'pattern') ?? null - const sortRaw = getOpt(parsed.opts, 'sort') - const sort = sortRaw && LIST_TEAM_SORTS.has(sortRaw as ListTeamSort) ? (sortRaw as ListTeamSort) : undefined - if (sortRaw && !sort) { - return { - code: 1, - out: '', - err: 'list-team: --sort must be company, team_uid, or name\n', - } - } - const verbose = hasOpt(parsed.opts, 'verbose') || hasOpt(parsed.opts, 'v') - const veryVerbose = hasOpt(parsed.opts, 'very-verbose') || hasOpt(parsed.opts, 'vv') - const rows = await v.listTeams!({ - pattern, - all: hasOpt(parsed.opts, 'all') || hasOpt(parsed.opts, 'a'), - verbose, - veryVerbose, - sort, - }) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } - } - if (rows.length === 0) { - return { code: 0, out: pattern ? `(no teams matched "${pattern}")\n` : '(no teams)\n', err: '' } - } - const table = formatTeamsTable(rows, { - style: 'commander', - showMember: verbose || veryVerbose, - }) - return { code: 0, out: renderTeamsAsciiTable(table) + '\n', err: '' } -} - -async function runWhoami(host: KeeperCliHost): Promise { - const v = host.getVault() - if (!v.isLoggedIn) { - return { code: 1, out: '', err: 'whoami: not logged in\n' } - } - const username = - (await host.getAccountUsername?.()) ?? host.envString('KEEPER_USER') ?? host.envString('KEEPER_USERNAME') - const summary = v.getSummary?.() - const lines = [`username: ${username ?? '(unknown)'}`] - if (summary) { - lines.push( - `records: ${summary.recordCount}`, - `folders: ${summary.folderCount}`, - `shared_folders: ${summary.sharedFolderCount}`, - `teams: ${summary.teamCount}` - ) - } - return { code: 0, out: lines.join('\n') + '\n', err: '' } -} - -export const listCommand: CliCommandDefinition = { - name: 'list', - order: 14, - aliases: ['l'], - description: 'List all vault records (Commander table).', - usage: 'list [--verbose|-v] [--json]', - flagOptions: ['--json', '--verbose', '-v'], - help: { - title: 'list — all records (Keeper Commander)', - synopsis: 'usage: list [--verbose]', - description: - ' Syncs and prints every record in a Commander-style table: uid, type, title, description, shared, and record category.', - options: ' --verbose, -v Do not truncate long columns (default max width 40).', - seeAlso: ' get, search, ls', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(listCommand), err: '' } - try { - return await runList(host, parsed) - } catch (e) { - return { code: 1, out: '', err: host.formatError('list', e) } - } - }, -} - -export const searchCommand: CliCommandDefinition = { - name: 'search', - order: 15, - aliases: ['s'], - description: 'Search vault records by text.', - usage: 'search [--json]', - flagOptions: ['--json', '--pattern'], - help: { - title: 'search — find records (Keeper Commander)', - synopsis: 'usage: search ', - description: ' Space-separated terms; all terms must match somewhere in the record.', - examples: ' search amazon\n search bank account', - seeAlso: ' get, ls', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(searchCommand), err: '' } - try { - return await runSearch(host, parsed) - } catch (e) { - return { code: 1, out: '', err: host.formatError('search', e) } - } - }, -} - -export const listSfCommand: CliCommandDefinition = { - name: 'list-sf', - order: 16, - aliases: ['lsf'], - description: 'List shared folders.', - usage: 'list-sf [pattern] [--verbose] [--json]', - flagOptions: ['--verbose', '-v', '--json', '--pattern'], - help: { - title: 'list-sf — shared folders (Keeper Commander)', - synopsis: 'usage: list-sf [pattern]', - seeAlso: ' ls, get', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(listSfCommand), err: '' } - try { - return await runListSf(host, parsed) - } catch (e) { - return { code: 1, out: '', err: host.formatError('list-sf', e) } - } - }, -} - -export const listTeamCommand: CliCommandDefinition = { - name: 'list-team', - order: 17, - aliases: ['lt'], - description: 'List teams (Commander table).', - usage: 'list-team [pattern] [--all|-a] [--verbose|-v] [--very-verbose|-vv] [--sort company|team_uid|name] [--json]', - flagOptions: [ - '--json', - '--pattern', - '--all', - '-a', - '--verbose', - '-v', - '--very-verbose', - '-vv', - '--sort', - ], - help: { - title: 'list-team — teams (Keeper Commander)', - synopsis: 'usage: list-team [pattern] [--all] [--verbose]', - description: - ' Lists teams from share contacts. Default: primary organization only. --all includes teams from other enterprises.', - options: ` --all, -a All teams in contacts (not just primary org) - --verbose, -v Include Member column (enterprise cache) - --very-verbose, -vv Fetch members via API when not cached - --sort company (default), team_uid, or name`, - seeAlso: ' get, whoami', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(listTeamCommand), err: '' } - try { - return await runListTeam(host, parsed) - } catch (e) { - return { code: 1, out: '', err: host.formatError('list-team', e) } - } - }, -} - -export const whoamiCommand: CliCommandDefinition = { - name: 'whoami', - order: 18, - description: 'Display current user and vault counts.', - usage: 'whoami', - help: { - title: 'whoami — current user (Keeper Commander)', - synopsis: 'usage: whoami', - seeAlso: ' login, sync-down', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(whoamiCommand), err: '' } - if (parsed.opts.size > 0 || parsed.positional.length > 0) { - return { code: 1, out: '', err: 'whoami: unexpected arguments\n' } - } - try { - return await runWhoami(host) - } catch (e) { - return { code: 1, out: '', err: host.formatError('whoami', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commander/nav.ts b/KeeperSdk/src/cli/commander/nav.ts deleted file mode 100644 index 0ff0e268..00000000 --- a/KeeperSdk/src/cli/commander/nav.ts +++ /dev/null @@ -1,234 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { formatTable } from '../table' - -function lsPath(parsed: ParsedCli): string | undefined { - return parsed.positional[0] -} - -function formatLs( - result: { - detail: boolean - folders: Array<{ uid: string; name: string }> - records: Array<{ uid: string; name: string; type?: string }> - }, - detail: boolean -): string { - if (result.folders.length + result.records.length === 0) return '(empty)\n' - - const headers = detail ? ['flags', 'uid', 'name', 'type'] : ['kind', 'uid', 'name'] - const rows: string[][] = [] - for (const f of result.folders) { - const flags = ((f as { flags?: string }).flags ?? '').trim() - rows.push(detail ? [flags || 'f---', f.uid, f.name, ''] : ['dir', f.uid, f.name]) - } - for (const r of result.records) { - const flags = ((r as { flags?: string }).flags ?? '').trim() - const type = r.type ?? '' - rows.push(detail ? [flags || 'r---', r.uid, r.name, type] : ['rec', r.uid, r.name]) - } - return formatTable(headers, rows) -} - -async function runLs(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listFolder', cmd) - if (cap) return cap - await v.sync() - - const detail = hasOpt(parsed.opts, 'detail') || hasOpt(parsed.opts, 'list') || hasOpt(parsed.opts, 'l') - const foldersOnly = hasOpt(parsed.opts, 'folders') || hasOpt(parsed.opts, 'f') - const recordsOnly = hasOpt(parsed.opts, 'records') || hasOpt(parsed.opts, 'r') - const target = lsPath(parsed) - - const listOpts = { - detail, - showFolders: recordsOnly ? false : true, - showRecords: foldersOnly ? false : true, - } - - if (!target) { - const result = await v.listFolder!({ ...listOpts }) - return { code: 0, out: formatLs(result, detail), err: '' } - } - - if (!v.changeDirectory || !v.getCurrentFolderUid) { - return { code: 1, out: '', err: `${cmd}: host lacks navigation capabilities.\n` } - } - - const originalUid = v.getCurrentFolderUid() - let resolvedUid: string | null - try { - const cd = await v.changeDirectory(target) - resolvedUid = cd.folderUid - } catch (e) { - return { code: 1, out: '', err: host.formatError(`${cmd} ${target}`, e) } - } - try { - const result = await v.listFolder!({ folderUid: resolvedUid ?? null, ...listOpts }) - return { code: 0, out: formatLs(result, detail), err: '' } - } finally { - if (resolvedUid !== originalUid) { - try { - await v.changeDirectory(originalUid ?? '/') - } catch { - /* best-effort */ - } - } - } -} - -async function runCd(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { - const target = parsed.positional[0] - if (!target) return { code: 1, out: '', err: `${cmd}: missing folder path. Usage: ${cmd} \n` } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'changeDirectory', cmd) - if (cap) return cap - try { - const res = await v.changeDirectory!(target) - return { code: 0, out: `${res.name}\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`${cmd} ${target}`, e) } - } -} - -async function runTree(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'tree', cmd) - if (cap) return cap - await v.sync() - const folderPath = parsed.positional[0] - const out = await v.tree!(folderPath ? { folderPath, showRecords: true } : { showRecords: true }) - return { code: 0, out: out.endsWith('\n') ? out : out + '\n', err: '' } -} - -async function runMkdir(host: KeeperCliHost, parsed: ParsedCli, cmd: string): Promise { - const target = parsed.positional[0] - if (!target) { - return { code: 1, out: '', err: `${cmd}: missing path. Usage: ${cmd} [-sf]\n` } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'mkdir', cmd) - if (cap) return cap - const cwd = v.getWorkingFolderDisplayName?.() ?? 'My Vault' - const shared = - hasOpt(parsed.opts, 'shared-folder') || - hasOpt(parsed.opts, 'sf') || - hasOpt(parsed.opts, 'shared') - try { - const res = await v.mkdir!(target, { sharedFolder: shared }) - if (!res.success) { - return { code: 1, out: '', err: `${cmd} [in ${cwd}]: ${res.message ?? 'failed'}\n` } - } - return { code: 0, out: `${res.folderUid}\t${target} (in ${cwd})\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`${cmd} ${target} [in ${cwd}]`, e) } - } -} - -const lsHelp: CliCommandDefinition['help'] = { - title: 'ls — list folder contents (Keeper Commander)', - synopsis: 'usage: ls [-l] [-f] [-r] [pattern]', - description: ' Lists records and subfolders in the current folder, or in PATH if given.', - options: ` -l, --list Detailed list (flags, types). - -f, --folders Folders only. - -r, --records Records only. - --help, -h Show this help.`, - examples: ' ls\n ls "Marketing"\n ls -l', - seeAlso: ' cd, tree, get', -} - -export const lsCommand: CliCommandDefinition = { - name: 'ls', - order: 11, - description: 'List folder contents (current folder or PATH).', - usage: 'ls [PATH] [-l|--list] [-f|--folders] [-r|--records]', - flagOptions: ['-l', '--list', '-f', '--folders', '-r', '--records', '--detail'], - help: lsHelp, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(lsCommand), err: '' } - try { - return await runLs(host, parsed, 'ls') - } catch (e) { - return { code: 1, out: '', err: host.formatError('ls', e) } - } - }, -} - -export const cdCommand: CliCommandDefinition = { - name: 'cd', - order: 12, - description: 'Change current folder.', - usage: 'cd ', - help: { - title: 'cd — change current folder (Keeper Commander)', - synopsis: 'usage: cd ', - description: ' PATH is a slash-separated folder name/UID sequence, or `/` for vault root.', - examples: ' cd Marketing\n cd ..\n cd /', - seeAlso: ' ls, tree', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(cdCommand), err: '' } - try { - return await runCd(host, parsed, 'cd') - } catch (e) { - return { code: 1, out: '', err: host.formatError('cd', e) } - } - }, -} - -export const treeCommand: CliCommandDefinition = { - name: 'tree', - order: 13, - description: 'Display the folder structure.', - usage: 'tree [PATH]', - help: { - title: 'tree — folder structure (Keeper Commander)', - synopsis: 'usage: tree [folder]', - description: - ' Renders an ASCII tree from PATH or the vault root. Each node is tagged [folder], [shared folder], or [record].', - seeAlso: ' ls, cd', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(treeCommand), err: '' } - try { - return await runTree(host, parsed, 'tree') - } catch (e) { - return { code: 1, out: '', err: host.formatError('tree', e) } - } - }, -} - -export const mkdirCommand: CliCommandDefinition = { - name: 'mkdir', - order: 14, - description: 'Create a folder.', - usage: 'mkdir [-sf|--shared-folder]', - flagOptions: ['-sf', '--shared-folder', '--shared'], - help: { - title: 'mkdir — create folder (Keeper Commander)', - synopsis: 'usage: mkdir [-sf]', - description: ' Creates a user folder under the current folder. -sf creates a shared folder.', - options: ' -sf, --shared-folder Create a shared folder.', - examples: ' mkdir Drafts\n mkdir TeamShare -sf', - seeAlso: ' cd, ls', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) return { code: 0, out: formatDetailedHelpForCommand(mkdirCommand), err: '' } - try { - return await runMkdir(host, parsed, 'mkdir') - } catch (e) { - return { code: 1, out: '', err: host.formatError('mkdir', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/help.ts b/KeeperSdk/src/cli/commands/help.ts deleted file mode 100644 index 24f49db6..00000000 --- a/KeeperSdk/src/cli/commands/help.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { CliCommandDefinition, KeeperCliHost } from '../types' -import { wantsCliHelp } from '../parse' -import { - formatAllCommandsSummary, - formatDetailedHelpForCommand, - formatShortCommandSummary, - getDetailedHelpPageForRegistry, -} from '../help' -import { isAuthCliCommand, listCliCommandsForLoginState } from '../access' -import { getCliCommand } from '../registry' - -export const helpCommand: CliCommandDefinition = { - name: 'help', - order: 0, - description: 'Show all commands, or full docs for one command (same as COMMAND --help).', - usage: 'help [command] (see also: help --help)', - help: { - title: 'help — show commands or short syntax for one command', - synopsis: ' help [COMMAND]', - description: ` Without arguments, lists commands for the current session. - When not logged in, only sign-in commands are listed (login, restore-session, …). - After login, lists vault commands as well. - - With COMMAND, prints usage for that command (sign-in commands only when logged out).`, - options: ' None. This command does not take GNU-style flags.', - seeAlso: ' Each command’s --help output.', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(helpCommand), err: '' } - } - if (parsed.opts.size > 0) { - return { code: 1, out: '', err: 'help: unknown option (try `help --help`)\n' } - } - const loggedIn = host.getVault().isLoggedIn - const visible = listCliCommandsForLoginState(loggedIn) - const args = parsed.positional - if (args.length === 0) { - if (loggedIn) { - return { code: 0, out: formatAllCommandsSummary(visible), err: '' } - } - return { - code: 0, - out: formatAllCommandsSummary(visible, { - header: 'Not logged in — sign-in commands:\n\n', - footer: - '\nRun `login`, `restore-session`, or `register-device` to open the vault.\n' + - 'After login, run `help` again for vault commands (get, ls, cd, …).\n', - }), - err: '', - } - } - if (args.length > 1) { - return { code: 1, out: '', err: 'Usage: help [command]\n' } - } - const name = args[0] - if (!loggedIn && !isAuthCliCommand(name)) { - return { - code: 1, - out: '', - err: - `help: "${name}" requires a logged-in session. ` + - 'Run `help` for sign-in commands (login, restore-session, register-device).\n', - } - } - const long = getDetailedHelpPageForRegistry(visible, name) - if (long) { - return { code: 0, out: long, err: '' } - } - const def = getCliCommand(name) - if (!def) { - return { code: 1, out: '', err: `help: unknown command: ${name}\n` } - } - return { code: 0, out: formatShortCommandSummary(def), err: '' } - }, -} diff --git a/KeeperSdk/src/cli/commands/login.ts b/KeeperSdk/src/cli/commands/login.ts deleted file mode 100644 index 25d9c294..00000000 --- a/KeeperSdk/src/cli/commands/login.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { utf8ToBase64Url } from '../utils' - -const LOGIN_ALLOWED = new Set([ - 'username', - 'user', - 'session-token', - 'token', - 'st', - 'session-token-plain', -]) - -export async function runLoginCommand(host: KeeperCliHost, parsed?: ParsedCli): Promise { - const opts = parsed?.opts ?? new Map() - if (parsed && wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(loginCommand), err: '' } - } - if (parsed) { - for (const secretFlag of ['password', 'pass', 'pwd'] as const) { - if (opts.has(secretFlag)) { - return { - code: 1, - out: '', - err: - 'login: do not pass --password on the command line (it is logged and visible). ' + - 'Use KEEPER_PASSWORD for automation, or run `login --username …` in the shell and enter the password when prompted (masked).\n', - } - } - } - const bad = rejectUnknownOptions(parsed, LOGIN_ALLOWED, 'login') - if (bad) return bad - } - - const username = getOpt(opts, 'username', 'user') ?? host.envString('KEEPER_USERNAME') - const passwordEnv = host.envString('KEEPER_PASSWORD') - const sessionRaw = getOpt(opts, 'session-token', 'token', 'st') ?? host.envString('KEEPER_SESSION_TOKEN') - const sessionPlain = parsed && hasOpt(opts, 'session-token-plain') - - if (parsed) { - const stPlainVal = opts.get('session-token-plain') - if (stPlainVal !== undefined && stPlainVal !== true) { - return { - code: 1, - out: '', - err: 'login: --session-token-plain is a boolean flag (no value)\n', - } - } - } - - if (!username) { - return { - code: 1, - out: '', - err: 'login: provide --username or KEEPER_USERNAME.\n', - } - } - - const sessionTrimmed = typeof sessionRaw === 'string' ? sessionRaw.trim() : '' - if (sessionTrimmed.length > 0) { - return loginWithSessionToken(host, username, sessionTrimmed, { plainToken: !!sessionPlain }) - } - - if (!passwordEnv) { - return { - code: 1, - needPassword: true, - loginUsername: username, - out: '', - err: '', - } - } - - return loginWithCredentials(host, username, passwordEnv) -} - -export async function loginWithCredentials( - host: KeeperCliHost, - username: string, - password: string -): Promise { - try { - const v = host.getVault() - if (v.isLoggedIn) { - await v.logout() - } - await v.login(username, password) - await v.sync() - return { code: 0, out: `keeper: logged in as ${username}.\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError('keeper', e) } - } -} - -export async function loginWithSessionToken( - host: KeeperCliHost, - username: string, - sessionToken: string, - options?: { plainToken?: boolean } -): Promise { - let token = sessionToken.trim() - if (options?.plainToken && token.length > 0) { - token = utf8ToBase64Url(token) - } - try { - const v = host.getVault() - if (v.isLoggedIn) { - await v.logout() - } - await v.loginWithSessionToken(username, token) - await v.sync() - return { code: 0, out: `keeper: logged in as ${username} (session token).\n`, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError('keeper', e) } - } -} - -/** Pass-through if logged in; auto-login when `KEEPER_USERNAME` is set; otherwise "not logged in". */ -export async function ensureLoggedIn(host: KeeperCliHost): Promise { - if (host.getVault().isLoggedIn) { - return { code: 0, out: '', err: '' } - } - if (host.envString('KEEPER_USERNAME')) { - return runLoginCommand(host, { positional: [], opts: new Map() }) - } - return { code: 1, out: '', err: 'not logged in\n' } -} - -export const loginCommand: CliCommandDefinition = { - name: 'login', - order: 10, - description: - 'Log in with password (env / masked prompt) or session token (flag or KEEPER_SESSION_TOKEN). Password never on CLI line.', - usage: - 'login [--username|--user ] [--session-token|--token|--st ] [--session-token-plain] [--help|-h]', - flagOptions: [ - '--user', - '--username', - '--session-token', - '--token', - '--st', - '--session-token-plain', - ], - allowedOptions: LOGIN_ALLOWED, - help: { - title: 'login — authenticate to Keeper (vault session)', - synopsis: ` login [--username|--user EMAIL_OR_NAME] - login [--username|--user U] [--session-token|--token|--st TOKEN] - login [--username|--user U] [--session-token TOKEN] [--session-token-plain]`, - description: ` Establishes a Keeper session. - - Username comes from --username / --user or KEEPER_USERNAME. - - Password MUST NOT appear on the CLI line (logging, proxies, browser history). - Automation: set KEEPER_PASSWORD in the environment when embedding in Node. - Web shell: run login with only a username; the UI prompts for a masked password - and sends it through the login transport, not in "line". - - Session token login: pass the token on the command line or via - KEEPER_SESSION_TOKEN (sensitive — same caveats as any secret on argv). - - --session-token-plain treats the value as plain UTF-8 and encodes base64url - before login (same idea as the session_token_login example). - - Device registration: session token login requires deviceToken + privateKey for - this host in session storage. Use register-device (or a prior password login in - this shell) to store them; see register-device --help.`, - options: ` --username, --user Account identifier (often email). - --session-token, --token, --st Session token string (or use KEEPER_SESSION_TOKEN). - --session-token-plain Treat --session-token value as plain UTF-8 and encode base64url.`, - environment: ` KEEPER_USERNAME Default username if not passed on the command line. - KEEPER_PASSWORD Password for non-interactive login (no session token). - KEEPER_SESSION_TOKEN Session token when not passed as a flag. - KEEPER_HOST Optional vault host / region (also: keeper-host attribute).`, - }, - run: (host, parsed) => runLoginCommand(host, parsed), -} diff --git a/KeeperSdk/src/cli/commands/logout.ts b/KeeperSdk/src/cli/commands/logout.ts deleted file mode 100644 index aa70924a..00000000 --- a/KeeperSdk/src/cli/commands/logout.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' - -export async function runLogoutCommand(host: KeeperCliHost, parsed?: ParsedCli): Promise { - if (parsed && wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(logoutCommand), err: '' } - } - if (parsed && parsed.opts.size > 0) { - return { code: 1, out: '', err: 'logout: no options (try: logout --help)\n' } - } - if (parsed && parsed.positional.length > 0) { - return { code: 1, out: '', err: 'Usage: logout\n' } - } - try { - const v = host.getVault() - if (!v.isLoggedIn) { - return { code: 0, out: 'keeper: already logged out.\n', err: '' } - } - await v.logout() - return { code: 0, out: 'keeper: logged out.\n', err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError('keeper', e) } - } -} - -export const logoutCommand: CliCommandDefinition = { - name: 'logout', - order: 200, - description: 'Log out of the current Keeper session.', - usage: 'logout [--help|-h]', - help: { - title: 'logout — end the current Keeper session', - synopsis: ' logout', - description: ' Ends the current session if one exists.', - options: ' None.', - }, - run: (host, parsed) => runLogoutCommand(host, parsed), -} diff --git a/KeeperSdk/src/cli/commands/registerDevice.ts b/KeeperSdk/src/cli/commands/registerDevice.ts deleted file mode 100644 index b7f2d5c8..00000000 --- a/KeeperSdk/src/cli/commands/registerDevice.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { CliCommandDefinition, KeeperCliHost } from '../types' -import { getOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' - -const REGISTER_DEVICE_ALLOWED = new Set(['device-token', 'dt', 'private-key', 'pk', 'username', 'user']) - -export const registerDeviceCommand: CliCommandDefinition = { - name: 'register-device', - order: 15, - description: - 'Store device token + private key in this shell’s session so session-token login works (see login --help).', - usage: - 'register-device --device-token|--dt --private-key|--pk [--username ] [--help|-h]', - flagOptions: [ - '--device-token', - '--dt', - '--private-key', - '--pk', - '--user', - '--username', - ], - allowedOptions: REGISTER_DEVICE_ALLOWED, - help: { - title: 'register-device — store device token and private key for session-token login', - synopsis: - ' register-device --device-token|--dt B64 --private-key|--pk B64 [--username|--user U]', - description: ` Saves device credentials for the current host in this shell’s in-memory - session. After this, you can run: - - login --username YOU --session-token TOKEN - - without a prior password login in this shell, as long as the token is valid. - - Obtain device_token and private_key from another machine’s keeper config after - a successful login, or from your integration that provisions device keys. - Values accept base64 or base64url (same decoding as SessionManager / normal64Bytes).`, - options: ` --device-token, --dt Device token string. - --private-key, --pk Device private key string. - --username, --user Optional; sets last username in session storage (recommended).`, - environment: ` REGISTER_DEVICE_TOKEN Same as --device-token when flag omitted. - REGISTER_DEVICE_PRIVATE_KEY Same as --private-key when flag omitted. - KEEPER_HOST Same as other keeper commands.`, - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(registerDeviceCommand), err: '' } - } - const bad = rejectUnknownOptions(parsed, REGISTER_DEVICE_ALLOWED, 'register-device') - if (bad) return bad - if (parsed.positional.length > 0) { - return { - code: 1, - out: '', - err: 'register-device: unexpected positional arguments\n', - } - } - - const deviceToken = - getOpt(parsed.opts, 'device-token', 'dt') ?? host.envString('REGISTER_DEVICE_TOKEN') - const privateKey = - getOpt(parsed.opts, 'private-key', 'pk') ?? host.envString('REGISTER_DEVICE_PRIVATE_KEY') - const usernameOpt = getOpt(parsed.opts, 'username', 'user') - const username = usernameOpt?.trim() || undefined - - const dt = typeof deviceToken === 'string' ? deviceToken.trim() : '' - const pk = typeof privateKey === 'string' ? privateKey.trim() : '' - if (!dt || !pk) { - return { - code: 1, - out: '', - err: - 'register-device: --device-token and --private-key required ' + - '(or REGISTER_DEVICE_TOKEN / REGISTER_DEVICE_PRIVATE_KEY env).\n', - } - } - - try { - const v = host.getVault() - await v.registerDevice(dt, pk, username ? { username } : undefined) - const umsg = username ? ` Last username set to ${username}.` : '' - return { - code: 0, - out: `keeper: device credentials stored in this shell’s session.${umsg} Next: login --username … --session-token …\n`, - err: '', - } - } catch (e) { - return { code: 1, out: '', err: host.formatError('register-device', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/restoreSession.ts b/KeeperSdk/src/cli/commands/restoreSession.ts deleted file mode 100644 index 7e416d59..00000000 --- a/KeeperSdk/src/cli/commands/restoreSession.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { CliCommandDefinition, KeeperCliHost, ParsedCli } from '../types' -import { - resolveSessionRestorePayload, - validateSessionRestoreInput, - type SessionRestoreInput, -} from '../../auth/sessionRestore' -import { getOpt, hasOpt, rejectUnknownOptions, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { runVaultSync } from './sync' - -/** Flags allowed to follow `--from-json ` on the same line. */ -export const RESTORE_SESSION_TRAILING_OPTS = [ - 'sync', - 'account-uid', - 'client-key', - 'data-key', - 'ecc-private-key', - 'ecc-public-key', - 'message-session-uid', - 'private-key', - 'session-token', - 'st', - 'session-token-type', - 'username', - 'user', - 'user-type', - 'sso-logout-url', - 'sso-session-id', - 'enterprise-public-key', - 'enterprise-ecc-public-key', -] as const - -const RESTORE_ALLOWED = new Set([ - 'sync', - 'from-json', - 'account-uid', - 'client-key', - 'data-key', - 'ecc-private-key', - 'ecc-public-key', - 'message-session-uid', - 'private-key', - 'session-token', - 'st', - 'session-token-type', - 'username', - 'user', - 'user-type', - 'sso-logout-url', - 'sso-session-id', - 'enterprise-public-key', - 'enterprise-ecc-public-key', -]) - -const ENV_PREFIX = 'RESTORE_SESSION_' - -const FIELD_ENV: Record = { - ACCOUNT_UID: 'accountUid', - CLIENT_KEY: 'clientKey', - DATA_KEY: 'dataKey', - ECC_PRIVATE_KEY: 'eccPrivateKey', - ECC_PUBLIC_KEY: 'eccPublicKey', - MESSAGE_SESSION_UID: 'messageSessionUid', - PRIVATE_KEY: 'privateKey', - SESSION_TOKEN: 'sessionToken', - SESSION_TOKEN_TYPE: 'sessionTokenType', - USERNAME: 'username', - USER_TYPE: 'userType', - SSO_LOGOUT_URL: 'ssoLogoutUrl', - SSO_SESSION_ID: 'ssoSessionId', - ENTERPRISE_PUBLIC_KEY: 'enterprisePublicKey', - ENTERPRISE_ECC_PUBLIC_KEY: 'enterpriseEccPublicKey', -} - -const OPT_TO_FIELD: Record = { - 'account-uid': 'accountUid', - 'client-key': 'clientKey', - 'data-key': 'dataKey', - 'ecc-private-key': 'eccPrivateKey', - 'ecc-public-key': 'eccPublicKey', - 'message-session-uid': 'messageSessionUid', - 'private-key': 'privateKey', - 'session-token': 'sessionToken', - st: 'sessionToken', - 'session-token-type': 'sessionTokenType', - username: 'username', - user: 'username', - 'user-type': 'userType', - 'sso-logout-url': 'ssoLogoutUrl', - 'sso-session-id': 'ssoSessionId', - 'enterprise-public-key': 'enterprisePublicKey', - 'enterprise-ecc-public-key': 'enterpriseEccPublicKey', -} - -function envField(host: KeeperCliHost, key: keyof SessionRestoreInput): string | undefined { - const envKey = Object.entries(FIELD_ENV).find(([, v]) => v === key)?.[0] - return envKey ? host.envString(`${ENV_PREFIX}${envKey}`) : undefined -} - -function buildInputFromFlags(host: KeeperCliHost, parsed: ParsedCli): SessionRestoreInput { - const partial: Partial = {} - - for (const [opt, field] of Object.entries(OPT_TO_FIELD)) { - const fromFlag = getOpt(parsed.opts, opt) - if (fromFlag !== undefined) { - ;(partial as Record)[field] = fromFlag - continue - } - const fromEnv = envField(host, field) - if (fromEnv !== undefined) { - ;(partial as Record)[field] = fromEnv - } - } - - return validateSessionRestoreInput(partial) -} - -export const restoreSessionCommand: CliCommandDefinition = { - name: 'restore-session', - order: 12, - description: - 'Restore a logged-in session from extension SessionParams (continueSession; no device keys required).', - usage: - 'restore-session --from-json FILE|JSON [--sync] OR restore-session --session-token … (see --help)', - flagOptions: [ - '--sync', - '--from-json', - '--account-uid', - '--client-key', - '--data-key', - '--ecc-private-key', - '--ecc-public-key', - '--message-session-uid', - '--private-key', - '--session-token', - '--session-token-type', - '--username', - '--user-type', - '--sso-logout-url', - '--sso-session-id', - '--enterprise-public-key', - '--enterprise-ecc-public-key', - ], - allowedOptions: RESTORE_ALLOWED, - help: { - title: 'restore-session — restore SessionParams from extension / vault export', - synopsis: ` restore-session --from-json session.json - restore-session --session-token TOKEN --username U --account-uid B64 …`, - description: ` Loads a full SessionParams snapshot and resumes the session (same path as - the browser extension after login). Use this when you have accountUid, - clientKey, dataKey, keys, sessionToken, username, etc. from extension storage - — deviceToken/device private key are not part of this payload. - - Provide parameters either as one JSON object (--from-json) or as flags / env. - Binary fields are base64 or base64url.`, - options: ` --from-json Inline JSON (object or JSON-stringified object), or a file path - The entire remainder of the command line is passed to JSON.parse (then file read if needed). - --account-uid, --client-key, --data-key, --ecc-private-key, --ecc-public-key - --message-session-uid, --private-key - --session-token, --st Session token string (as stored; often base64url) - --session-token-type Numeric SessionTokenType enum - --username, --user - --user-type 0=normal, 1=cloud_sso, 2=onsite_sso (or string names) - --sso-logout-url, --sso-session-id - --enterprise-public-key, --enterprise-ecc-public-key (optional) - --sync Run syncDown after restoring the session`, - environment: ` RESTORE_SESSION_JSON Same as --from-json - RESTORE_SESSION_ACCOUNT_UID Per-field overrides (see --help flags) - RESTORE_SESSION_SESSION_TOKEN - … (RESTORE_SESSION_ for each field above)`, - note: ' sessionToken expires; region must match keeper-host / KEEPER_HOST.', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(restoreSessionCommand), err: '' } - } - const bad = rejectUnknownOptions(parsed, RESTORE_ALLOWED, 'restore-session') - if (bad) return bad - if (parsed.positional.length > 0) { - return { code: 1, out: '', err: 'restore-session: unexpected positional arguments\n' } - } - - try { - let input: SessionRestoreInput - const jsonRaw = getOpt(parsed.opts, 'from-json') ?? host.envString('RESTORE_SESSION_JSON') - if (jsonRaw) { - const readFile = - host.readTextFile ?? - (typeof document === 'undefined' - ? async (path: string) => (await import('fs/promises')).readFile(path, 'utf8') - : undefined) - input = await resolveSessionRestorePayload(jsonRaw, readFile) - } else { - input = buildInputFromFlags(host, parsed) - } - - await host.getVault().restoreSession(input) - let out = `keeper: session restored for ${input.username}.\n` - if (hasOpt(parsed.opts, 'sync')) { - const syncResult = await runVaultSync(host) - if (syncResult.code !== 0) { - return syncResult - } - out += syncResult.out - } - return { code: 0, out, err: '' } - } catch (e) { - return { code: 1, out: '', err: host.formatError('restore-session', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/sync.ts b/KeeperSdk/src/cli/commands/sync.ts deleted file mode 100644 index 72d71f67..00000000 --- a/KeeperSdk/src/cli/commands/sync.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { SyncResult } from '@keeper-security/keeperapi' -import type { CliCommandDefinition, CliResult, KeeperCliHost } from '../types' -import { wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureLoggedIn } from './login' - -function formatSyncSummary(result: SyncResult): string { - const lines = [`keeper: sync complete for ${result.username}.`, ` pages: ${result.pageCount}`] - if (result.totalTime) lines.push(` total: ${result.totalTime}`) - if (result.networkTime) lines.push(` network: ${result.networkTime}`) - const counts = result.counts ?? {} - const parts = Object.entries(counts) - .filter(([, n]) => typeof n === 'number' && n > 0) - .map(([k, n]) => `${k}=${n}`) - if (parts.length) lines.push(` counts: ${parts.join(', ')}`) - if (result.error) lines.push(` warning: ${result.error}`) - return lines.join('\n') + '\n' -} - -/** Download vault data via keeperapi syncDown (KeeperVault.sync). */ -export async function runVaultSync(host: KeeperCliHost): Promise { - const v = host.getVault() - if (!v.isLoggedIn) { - const login = await ensureLoggedIn(host) - if (login.code !== 0) return login - } - const result = await v.sync() - return { code: 0, out: formatSyncSummary(result), err: '' } -} - -export const syncCommand: CliCommandDefinition = { - name: 'sync', - order: 20, - aliases: ['syncdown', 'sync-down', 'd'], - description: 'Download / refresh vault data from Keeper (syncDown).', - usage: 'sync [--help|-h]', - help: { - title: 'sync — download vault data (syncDown)', - synopsis: ' sync', - description: ` Pulls records, folders, and related vault data into local storage. - Requires an active session (login or restore-session).`, - options: ' --help, -h Show this help.', - seeAlso: ' restore-session --sync, list, ls', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(syncCommand), err: '' } - } - if (parsed.opts.size > 0) { - return { code: 1, out: '', err: 'sync: unknown option (try: sync --help)\n' } - } - if (parsed.positional.length > 0) { - return { code: 1, out: '', err: 'sync: unexpected arguments (try: sync --help)\n' } - } - try { - return await runVaultSync(host) - } catch (e) { - return { code: 1, out: '', err: host.formatError('sync', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/users.ts b/KeeperSdk/src/cli/commands/users.ts deleted file mode 100644 index 032b7660..00000000 --- a/KeeperSdk/src/cli/commands/users.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { getOpt, hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' -import { - formatUsersTable, - renderUsersAsciiTable, - SUPPORTED_USER_COLUMNS, -} from '../../users/listUsers' -import type { UserColumnInput } from '../../users/userTypes' -import { formatUserView, userViewTable } from '../../users/viewUser' - -const SUBCOMMANDS = ['list', 'view'] as const -type Sub = (typeof SUBCOMMANDS)[number] - -function parseColumns(raw: string | undefined): UserColumnInput[] | '*' | undefined { - const trimmed = raw?.trim() - if (!trimmed) return undefined - if (trimmed === '*') return '*' - return trimmed - .split(',') - .map((p) => p.trim()) - .filter((p) => p.length > 0) as UserColumnInput[] -} - -async function runList(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'listUsers', 'users list') - if (cap) return cap - await v.sync!() - const pattern = getOpt(parsed.opts, 'pattern') ?? null - const columns = parseColumns(getOpt(parsed.opts, 'columns')) - const rows = await v.listUsers!({ pattern, columns }) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(rows, null, 2) + '\n', err: '' } - } - if (rows.length === 0) { - return { - code: 0, - out: pattern ? `(no users matched "${pattern}")\n` : '(no users)\n', - err: '', - } - } - const table = formatUsersTable(rows, { columns }) - return { code: 0, out: renderUsersAsciiTable(table) + '\n', err: '' } -} - -async function runView(host: KeeperCliHost, parsed: ParsedCli): Promise { - const id = parsed.positional[1] - if (!id) { - return { code: 1, out: '', err: 'users view: missing email or user id. Usage: users view \n' } - } - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'viewUser', 'users view') - if (cap) return cap - const view = await v.viewUser!(id) - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(view, null, 2) + '\n', err: '' } - } - const formatted = formatUserView(view, { verbose: hasOpt(parsed.opts, 'verbose') }) - return { code: 0, out: userViewTable(formatted) + '\n', err: '' } -} - -export const usersCommand: CliCommandDefinition = { - name: 'users', - order: 41, - description: 'Enterprise users (list, view).', - usage: 'users list|view [args] [--pattern P] [--columns C] [--json] [--verbose] [--help|-h]', - subcommands: [...SUBCOMMANDS], - flagOptions: ['--pattern', '--columns', '--json', '--verbose'], - help: { - title: 'users — list and inspect enterprise users', - synopsis: ` users list [--pattern P] [--columns cols] - users view EMAIL|ID [--json] [--verbose]`, - description: ' Requires an enterprise account.', - arguments: ` list Table of enterprise users. - view Details for one user by email or enterprise_user_id.`, - options: ` --pattern list only: filter by name/email substring. - --columns list only: comma-separated columns or * (supported: ${SUPPORTED_USER_COLUMNS.join(', ')}). - --json Emit JSON instead of a table. - --verbose view only: include UIDs in team/role rows. - --help, -h Show this help.`, - examples: ` users list - users list --pattern @acme.com --columns name,status,node - users view user@example.com`, - seeAlso: ' list-team, whoami, sync, login', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(usersCommand), err: '' } - } - const sub = (parsed.positional[0]?.toLowerCase() ?? 'list') as Sub - if (!SUBCOMMANDS.includes(sub)) { - return { - code: 1, - out: '', - err: `users: unknown subcommand "${parsed.positional[0]}". Try: users --help\n`, - } - } - try { - switch (sub) { - case 'list': - return await runList(host, parsed) - case 'view': - return await runView(host, parsed) - } - } catch (e) { - return { code: 1, out: '', err: host.formatError(`users ${sub}`, e) } - } - }, -} diff --git a/KeeperSdk/src/cli/commands/vault.ts b/KeeperSdk/src/cli/commands/vault.ts deleted file mode 100644 index d3c25482..00000000 --- a/KeeperSdk/src/cli/commands/vault.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from '../types' -import { hasOpt, wantsCliHelp } from '../parse' -import { formatDetailedHelpForCommand } from '../help' -import { ensureCapability, ensureSession } from '../commandHelpers' - -async function runSummary(host: KeeperCliHost, parsed: ParsedCli): Promise { - const r = await ensureSession(host) - if (r) return r - const v = host.getVault() - const cap = ensureCapability(v, 'getSummary', 'vault summary') - if (cap) return cap - await v.sync!() - const summary = v.getSummary!() - if (hasOpt(parsed.opts, 'json')) { - return { code: 0, out: JSON.stringify(summary, null, 2) + '\n', err: '' } - } - const lines = [ - `records: ${summary.recordCount}`, - `shared_folders: ${summary.sharedFolderCount}`, - `teams: ${summary.teamCount}`, - `folders: ${summary.folderCount}`, - ] - return { code: 0, out: lines.join('\n') + '\n', err: '' } -} - -export const vaultCommand: CliCommandDefinition = { - name: 'vault', - order: 25, - description: 'Vault summary counts (records, folders, teams, …).', - usage: 'vault summary [--json] [--help|-h]', - subcommands: ['summary'], - flagOptions: ['--json'], - help: { - title: 'vault — vault-wide statistics', - synopsis: ' vault summary [--json]', - description: ' Runs sync, then prints counts from the local vault cache.', - arguments: ' summary Print record, shared folder, team, and user-folder counts.', - options: ' --json Emit JSON.\n --help, -h Show this help.', - examples: ' vault summary\n vault summary --json', - seeAlso: ' sync, list, tree, whoami', - }, - async run(host, parsed) { - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(vaultCommand), err: '' } - } - const sub = parsed.positional[0]?.toLowerCase() ?? 'summary' - if (sub !== 'summary') { - return { code: 1, out: '', err: 'Usage: vault summary\n' } - } - try { - return await runSummary(host, parsed) - } catch (e) { - return { code: 1, out: '', err: host.formatError('vault summary', e) } - } - }, -} diff --git a/KeeperSdk/src/cli/dispatch.ts b/KeeperSdk/src/cli/dispatch.ts deleted file mode 100644 index b3be1427..00000000 --- a/KeeperSdk/src/cli/dispatch.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { CliResult, KeeperCliHost, ParsedCli } from './types' -import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' -import { extractFromJsonFlagValue } from './jsonArg' -import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' -import { formatDetailedHelpForCommand } from './help' -import { isAuthCliCommand } from './access' -import { getCliCommand } from './registry' - -const NOT_LOGGED_IN_ERR = - 'Not logged in. Run `login`, `restore-session`, or `register-device` (see `help`).\n' - -export async function dispatchKeeperCli( - commandName: string, - args: string[], - host: KeeperCliHost, - preParsed?: ParsedCli -): Promise { - const def = getCliCommand(commandName) - if (!def) { - return { code: 1, out: '', err: `Unknown command: ${commandName}\n` } - } - if (!host.getVault().isLoggedIn && !isAuthCliCommand(def.name)) { - return { code: 1, out: '', err: NOT_LOGGED_IN_ERR } - } - const parsed = preParsed ?? parseCliArgs(args) - if (wantsCliHelp(parsed)) { - return { code: 0, out: formatDetailedHelpForCommand(def), err: '' } - } - return def.run(host, parsed) -} - -export async function dispatchCliLine(line: string, host: KeeperCliHost): Promise { - const trimmed = line.trim() - if (!trimmed) { - return { code: 0, out: '', err: '' } - } - const tokens = tokenizeArguments(trimmed) - const name = tokens[0]?.toLowerCase() - if (!name) { - return { code: 0, out: '', err: '' } - } - const args = tokens.slice(1) - let preParsed: ParsedCli | undefined - if (name === 'restore-session') { - const json = extractFromJsonFlagValue(trimmed, 'from-json', RESTORE_SESSION_TRAILING_OPTS) - if (json) { - preParsed = parseCliArgs(args) - preParsed.opts.set('from-json', json) - } - } - return dispatchKeeperCli(name, args, host, preParsed) -} diff --git a/KeeperSdk/src/cli/help.ts b/KeeperSdk/src/cli/help.ts deleted file mode 100644 index 40773a4f..00000000 --- a/KeeperSdk/src/cli/help.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { CliCommandDefinition, CliHelpDoc } from './types' - -const SECTION_ORDER: (keyof CliHelpDoc)[] = [ - 'synopsis', - 'description', - 'arguments', - 'options', - 'environment', - 'examples', - 'seeAlso', - 'note', -] - -const SECTION_LABELS: Partial> = { - synopsis: 'SYNOPSIS', - description: 'DESCRIPTION', - arguments: 'ARGUMENTS', - options: 'OPTIONS', - environment: 'ENVIRONMENT', - examples: 'EXAMPLES', - seeAlso: 'SEE ALSO', - note: 'NOTE', -} - -export function formatDetailedHelp(doc: CliHelpDoc): string { - const parts: string[] = [doc.title.trim()] - for (const key of SECTION_ORDER) { - const body = doc[key] - if (typeof body !== 'string' || !body.trim()) continue - const label = SECTION_LABELS[key] - if (label) { - parts.push('') - parts.push(label) - } - parts.push(body.trim()) - } - return `${parts.join('\n')}\n` -} - -export function formatDetailedHelpForCommand(def: CliCommandDefinition): string { - return formatDetailedHelp(def.help) -} - -export function getDetailedHelpPageForRegistry( - commands: Iterable, - name: string -): string | null { - const key = name.toLowerCase() - for (const def of commands) { - if (def.name === key) return formatDetailedHelpForCommand(def) - if (def.aliases?.some((a) => a.toLowerCase() === key)) { - return formatDetailedHelpForCommand(def) - } - } - return null -} - -export type CommandsSummaryOptions = { - header?: string - footer?: string -} - -export function formatAllCommandsSummary( - commands: readonly CliCommandDefinition[], - options?: CommandsSummaryOptions -): string { - const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name)) - const w = Math.max(...sorted.map((c) => c.name.length), 8) - let out = options?.header ?? 'Supported commands:\n\n' - if (!out.endsWith('\n\n')) { - out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n` - } - for (const c of sorted) { - out += ` ${c.name.padEnd(w)} ${c.description}\n` - } - out += options?.footer ?? '\nRun ` --help` (or `-h`) for details on a specific command.\n' - return out -} - -export function formatShortCommandSummary(def: CliCommandDefinition): string { - return `${def.name} — ${def.description}\n Usage: ${def.usage}\n` -} diff --git a/KeeperSdk/src/cli/index.ts b/KeeperSdk/src/cli/index.ts deleted file mode 100644 index 8d150d61..00000000 --- a/KeeperSdk/src/cli/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { registerBuiltinCliCommands } from './builtinCommands' -import { registerCliAlias } from './registry' - -let registryInitialized = false - -/** Register built-in Keeper CLI commands (idempotent). */ -export function ensureKeeperCliRegistry(): void { - if (registryInitialized) return - registryInitialized = true - registerBuiltinCliCommands() - registerCliAlias('?', 'help') -} - -ensureKeeperCliRegistry() - -export type { - CliResult, - ParsedCli, - CliHelpDoc, - CliCommandDefinition, - KeeperCliHost, - KeeperCliVault, -} from './types' - -export { - tokenizeArguments, - parseCliArgs, - hasOpt, - getOpt, - wantsCliHelp, - rejectUnknownOptions, -} from './parse' - -export { - formatDetailedHelp, - formatDetailedHelpForCommand, - formatAllCommandsSummary, - formatShortCommandSummary, -} from './help' -import { getDetailedHelpPageForRegistry } from './help' -import { listCliCommands } from './registry' - -export function getDetailedHelpPage(name: string): string | null { - ensureKeeperCliRegistry() - return getDetailedHelpPageForRegistry(listCliCommands(), name) -} - -export { - registerCliCommand, - registerCliAlias, - resolveCliCommandName, - getCliCommand, - listCliCommands, - listCliCommandNames, - listDocumentedCommands, - clearCliRegistry, -} from './registry' - -export { - AUTH_CLI_COMMAND_NAMES, - isAuthCliCommand, - filterCliCommandsForLoginState, - listCliCommandsForLoginState, - listCliCommandNamesForLoginState, -} from './access' - -export { dispatchKeeperCli, dispatchCliLine } from './dispatch' - -export { KeeperCliParser, createKeeperCliParser } from './parser' -export type { KeeperCliParserOptions } from './parser' - -export { - runLoginCommand, - loginWithCredentials, - loginWithSessionToken, - ensureLoggedIn, - loginCommand, -} from './commands/login' - -export { runLogoutCommand, logoutCommand } from './commands/logout' -export { usersCommand } from './commands/users' -export { vaultCommand } from './commands/vault' -export { registerDeviceCommand } from './commands/registerDevice' -export { helpCommand } from './commands/help' -export { restoreSessionCommand } from './commands/restoreSession' -export { syncCommand, runVaultSync } from './commands/sync' - -export { getKeeperCliPromptPrefix } from './prompt' -export { BUILTIN_CLI_COMMANDS, registerBuiltinCliCommands } from './builtinCommands' -export { - getCommand, - executeGet, - listCommand, - searchCommand, - listSfCommand, - listTeamCommand, - whoamiCommand, - lsCommand, - cdCommand, - treeCommand, - mkdirCommand, -} from './commander' - -export { utf8ToBase64Url, recordUid } from './utils' - -export type { SessionRestoreInput } from '../auth/sessionRestore' -export { - toSessionParams, - validateSessionRestoreInput, - sessionRestoreFromJson, - resolveSessionRestorePayload, -} from '../auth/sessionRestore' diff --git a/KeeperSdk/src/cli/jsonArg.ts b/KeeperSdk/src/cli/jsonArg.ts deleted file mode 100644 index 6a11c1bc..00000000 --- a/KeeperSdk/src/cli/jsonArg.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Everything after `--from-json` on the command line (trimmed). No tokenization — callers use JSON.parse. - * Trailing flags such as `--sync` are stripped via {@link stripTrailingCliFlags}. - */ -export function extractFromJsonFlagValue( - line: string, - flag = 'from-json', - trailingFlags: readonly string[] = [] -): string | null { - const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const flagRe = new RegExp(`(?:^|\\s)--${escaped}(?:\\s*=\\s*|\\s+)`, 'i') - const m = line.match(flagRe) - if (!m || m.index === undefined) return null - const rest = line.slice(m.index + m[0].length).trim() - if (!rest) return null - return stripTrailingCliFlags(rest, trailingFlags) -} - -/** Remove trailing ` --flag` tokens (e.g. `--sync` after a file path or JSON blob). */ -export function stripTrailingCliFlags(value: string, flagNames: readonly string[]): string { - if (flagNames.length === 0) return value.trim() - let s = value.trim() - const parts = flagNames.map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - const alt = parts.join('|') - const tailFlag = new RegExp(`\\s+--(?:${alt})(?:\\s*=\\s*(?:"[^"]*"|\\S+))?\\s*$`, 'i') - while (tailFlag.test(s)) { - s = s.replace(tailFlag, '').trim() - } - return s -} diff --git a/KeeperSdk/src/cli/parse.ts b/KeeperSdk/src/cli/parse.ts deleted file mode 100644 index 73f9ef15..00000000 --- a/KeeperSdk/src/cli/parse.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { CliResult, ParsedCli } from './types' - -const isWhitespace = (ch: string) => /\s/.test(ch) - -/** Split a command line into tokens; respects double quotes and `\\` escapes. */ -export function tokenizeArguments(args: string): string[] { - const out: string[] = [] - const sb: string[] = [] - let pos = 0 - let inQuote = false - let escape = false - - const flush = () => { - if (sb.length > 0) { - out.push(sb.join('')) - sb.length = 0 - } - } - - while (pos < args.length) { - const ch = args[pos] - if (escape) { - escape = false - sb.push(ch) - pos++ - continue - } - if (inQuote) { - if (ch === '\\') { - escape = true - pos++ - continue - } - if (ch === '"') { - inQuote = false - pos++ - continue - } - sb.push(ch) - pos++ - continue - } - switch (ch) { - case '\\': - escape = true - pos++ - break - case '"': - inQuote = true - pos++ - break - default: - if (isWhitespace(ch)) { - flush() - pos++ - } else { - sb.push(ch) - pos++ - } - } - } - flush() - return out -} - -function setBool(opts: Map, k: string): void { - opts.set(k.toLowerCase(), true) -} - -function setStr(opts: Map, k: string, v: string): void { - opts.set(k.toLowerCase(), v) -} - -/** Parse argv-style tokens after the command name. */ -export function parseCliArgs(tokens: string[]): ParsedCli { - const positional: string[] = [] - const opts = new Map() - - let i = 0 - while (i < tokens.length) { - const t = tokens[i] - if (t === '--') { - positional.push(...tokens.slice(i + 1)) - break - } - if (t === '-' || !t.startsWith('-')) { - positional.push(t) - i++ - continue - } - - if (t.startsWith('--')) { - const body = t.slice(2) - if (!body) { - positional.push(t) - i++ - continue - } - const eq = body.indexOf('=') - if (eq >= 0) { - setStr(opts, body.slice(0, eq), body.slice(eq + 1)) - i++ - continue - } - const name = body - const next = tokens[i + 1] - if (next && next !== '--' && !next.startsWith('-')) { - setStr(opts, name, next) - i += 2 - continue - } - setBool(opts, name) - i++ - continue - } - - const rest = t.slice(1) - if (!rest) { - positional.push(t) - i++ - continue - } - if (/^[A-Za-z]$/.test(rest)) { - setBool(opts, rest) - i++ - continue - } - if (/^[A-Za-z]+$/.test(rest)) { - for (const ch of rest) setBool(opts, ch) - i++ - continue - } - - positional.push(t) - i++ - } - - return { positional, opts } -} - -export function hasOpt(opts: Map, ...names: string[]): boolean { - for (const n of names) { - const v = opts.get(n.toLowerCase()) - if (v === true) return true - } - return false -} - -export function getOpt(opts: Map, ...names: string[]): string | undefined { - for (const n of names) { - const v = opts.get(n.toLowerCase()) - if (v !== undefined && v !== true) return v - } - return undefined -} - -export function wantsCliHelp(parsed: ParsedCli): boolean { - return hasOpt(parsed.opts, 'help', 'h') -} - -export function rejectUnknownOptions( - parsed: ParsedCli, - allowed: ReadonlySet, - commandName: string -): CliResult | null { - for (const k of parsed.opts.keys()) { - if (k === 'help' || k === 'h') continue - if (!allowed.has(k)) { - return { code: 1, out: '', err: `${commandName}: unknown option --${k}\n` } - } - } - return null -} diff --git a/KeeperSdk/src/cli/parser.ts b/KeeperSdk/src/cli/parser.ts deleted file mode 100644 index 7fa74311..00000000 --- a/KeeperSdk/src/cli/parser.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { CliCommandDefinition, CliResult, KeeperCliHost, ParsedCli } from './types' -import { parseCliArgs, tokenizeArguments, wantsCliHelp } from './parse' -import { extractFromJsonFlagValue } from './jsonArg' -import { RESTORE_SESSION_TRAILING_OPTS } from './commands/restoreSession' -import { AUTH_CLI_COMMAND_NAMES, isAuthCliCommand } from './access' -import { BUILTIN_CLI_COMMANDS } from './builtinCommands' -import { formatAllCommandsSummary, formatDetailedHelpForCommand, formatShortCommandSummary } from './help' - -const NOT_LOGGED_IN_ERR = - 'Not logged in. Run `login`, `restore-session`, or `register-device` (see `help`).\n' - -export type KeeperCliParserOptions = { - prog?: string - description?: string - epilog?: string -} - -/** Self-contained CLI parser. Register commands, then `parse()` dispatches a line. */ -export class KeeperCliParser { - private readonly prog: string - private readonly description: string - private readonly epilog?: string - private readonly commands = new Map() - private readonly aliases = new Map() - - constructor(options: KeeperCliParserOptions = {}) { - this.prog = options.prog ?? 'keeper' - this.description = options.description ?? '' - this.epilog = options.epilog - } - - addCommand(def: CliCommandDefinition): this { - const key = def.name.toLowerCase() - this.commands.set(key, def) - if (def.aliases) { - for (const alias of def.aliases) { - this.aliases.set(alias.toLowerCase(), key) - } - } - return this - } - - addCommands(defs: Iterable): this { - for (const def of defs) this.addCommand(def) - return this - } - - list(): CliCommandDefinition[] { - return [...this.commands.values()].sort((a, b) => { - const oa = a.order ?? 500 - const ob = b.order ?? 500 - if (oa !== ob) return oa - ob - return a.name.localeCompare(b.name) - }) - } - - listNames(): string[] { - return this.list().map((c) => c.name) - } - - resolve(name: string): CliCommandDefinition | undefined { - const key = name.toLowerCase() - if (this.commands.has(key)) return this.commands.get(key) - const target = this.aliases.get(key) - return target ? this.commands.get(target) : undefined - } - - formatHelp(host?: KeeperCliHost): string { - const loggedIn = host?.getVault().isLoggedIn ?? true - const commands = loggedIn - ? this.list() - : this.list().filter((c) => AUTH_CLI_COMMAND_NAMES.has(c.name)) - const header = this.description ? `${this.prog} — ${this.description}\n\n` : '' - const body = loggedIn - ? formatAllCommandsSummary(commands) - : formatAllCommandsSummary(commands, { - header: 'Not logged in — sign-in commands:\n\n', - footer: - '\nRun `login`, `restore-session`, or `register-device` to open the vault.\n' + - 'After login, run `help` again for vault commands (get, ls, cd, …).\n', - }) - const footer = this.epilog ? `\n${this.epilog}\n` : '' - return header + body + footer - } - - formatCommandHelp(name: string): string | null { - const def = this.resolve(name) - return def ? formatDetailedHelpForCommand(def) : null - } - - formatCommandSummary(name: string): string | null { - const def = this.resolve(name) - return def ? formatShortCommandSummary(def) : null - } - - async parse(line: string | readonly string[], host: KeeperCliHost): Promise { - const { tokens, raw } = normalizeInput(line) - if (tokens.length === 0) { - return ok(this.formatHelp(host)) - } - - const first = tokens[0] - const rest = tokens.slice(1) - - if (isHelpToken(first)) { - const sub = rest[0] - if (!sub) return ok(this.formatHelp(host)) - if (!host.getVault().isLoggedIn && !isAuthCliCommand(sub)) { - return err(NOT_LOGGED_IN_ERR) - } - const page = this.formatCommandHelp(sub) - if (page) return ok(page) - return err(`${this.prog}: unknown command: ${sub}\nTry: ${this.prog} --help\n`) - } - - const def = this.resolve(first) - if (!def) { - return err(`${this.prog}: unknown command: ${first}\nTry: ${this.prog} --help\n`) - } - - if (!host.getVault().isLoggedIn && !isAuthCliCommand(def.name)) { - return err(NOT_LOGGED_IN_ERR) - } - - let parsed: ParsedCli - if (def.name === 'restore-session') { - const json = extractFromJsonFlagValue(raw, 'from-json', RESTORE_SESSION_TRAILING_OPTS) - parsed = parseCliArgs(rest) - if (json) parsed.opts.set('from-json', json) - } else { - parsed = parseCliArgs(rest) - } - - if (wantsCliHelp(parsed)) { - return ok(formatDetailedHelpForCommand(def)) - } - return def.run(host, parsed) - } -} - -/** Parser pre-loaded with the SDK's built-in commands. */ -export function createKeeperCliParser(options: KeeperCliParserOptions = {}): KeeperCliParser { - const parser = new KeeperCliParser(options) - void loadBuiltinsInto(parser) - return parser -} - -function loadBuiltinsInto(parser: KeeperCliParser): void { - parser.addCommands(BUILTIN_CLI_COMMANDS) -} - -function normalizeInput(line: string | readonly string[]): { tokens: string[]; raw: string } { - if (typeof line === 'string') { - const trimmed = line.trim() - return { tokens: trimmed ? tokenizeArguments(trimmed) : [], raw: trimmed } - } - const tokens = [...line].filter((t) => t.length > 0) - return { tokens, raw: tokens.join(' ') } -} - -function isHelpToken(token: string): boolean { - const t = token.toLowerCase() - return t === '--help' || t === '-h' || t === 'help' -} - -function ok(out: string): CliResult { - return { code: 0, out, err: '' } -} - -function err(message: string): CliResult { - return { code: 1, out: '', err: message } -} diff --git a/KeeperSdk/src/cli/prompt.ts b/KeeperSdk/src/cli/prompt.ts deleted file mode 100644 index 0ac8ea99..00000000 --- a/KeeperSdk/src/cli/prompt.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { KeeperCliHost } from './types' - -const NOT_LOGGED_IN_PROMPT = 'Not logged in> ' -const PROMPT_MAX_LEN = 40 - -export function getKeeperCliPromptPrefix(host: KeeperCliHost): string { - const v = host.getVault() - if (!v.isLoggedIn) return NOT_LOGGED_IN_PROMPT - const name = v.getWorkingFolderDisplayName?.() ?? 'My Vault' - const label = name.length > PROMPT_MAX_LEN ? `...${name.slice(-37)}` : name - return `${label}> ` -} diff --git a/KeeperSdk/src/cli/registry.ts b/KeeperSdk/src/cli/registry.ts deleted file mode 100644 index 4e541bac..00000000 --- a/KeeperSdk/src/cli/registry.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { CliCommandDefinition } from './types' - -const commands = new Map() -const aliases = new Map() - -function normalizeName(name: string): string { - return name.toLowerCase() -} - -export function registerCliCommand(def: CliCommandDefinition): void { - const key = normalizeName(def.name) - commands.set(key, def) - if (def.aliases) { - for (const alias of def.aliases) { - aliases.set(normalizeName(alias), key) - } - } -} - -export function registerCliAlias(alias: string, commandName: string): void { - aliases.set(normalizeName(alias), normalizeName(commandName)) -} - -export function resolveCliCommandName(name: string): string | undefined { - const key = normalizeName(name) - if (commands.has(key)) return key - return aliases.get(key) -} - -export function getCliCommand(name: string): CliCommandDefinition | undefined { - const key = resolveCliCommandName(name) - return key ? commands.get(key) : undefined -} - -export function listCliCommands(): CliCommandDefinition[] { - return [...commands.values()].sort((a, b) => { - const oa = a.order ?? 500 - const ob = b.order ?? 500 - if (oa !== ob) return oa - ob - return a.name.localeCompare(b.name) - }) -} - -export function listCliCommandNames(): readonly string[] { - return listCliCommands().map((c) => c.name) -} - -export function listDocumentedCommands(): readonly string[] { - return listCliCommandNames() -} - -export function clearCliRegistry(): void { - commands.clear() - aliases.clear() -} diff --git a/KeeperSdk/src/cli/table.ts b/KeeperSdk/src/cli/table.ts deleted file mode 100644 index 499f206c..00000000 --- a/KeeperSdk/src/cli/table.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** Fixed-width column formatter. Last column is left unpadded. */ -export function formatTable(headers: string[], rows: string[][]): string { - if (rows.length === 0) return '' - const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))) - const fmt = (cells: string[]): string => - cells.map((s, i) => (i === cells.length - 1 ? s : (s ?? '').padEnd(widths[i]))).join(' ') - return [fmt(headers), ...rows.map(fmt)].join('\n') + '\n' -} diff --git a/KeeperSdk/src/cli/types.ts b/KeeperSdk/src/cli/types.ts deleted file mode 100644 index eb9f76a5..00000000 --- a/KeeperSdk/src/cli/types.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { DRecord, DSharedFolder, SyncResult } from '@keeper-security/keeperapi' -import type { SessionRestoreInput } from '../auth/sessionRestore' -import type { ChangeDirectoryResult } from '../folders/changeDirectory' -import type { FolderTreeBuildOptions } from '../folders/folderTree' -import type { GetFolderOptions, GetFolderResult } from '../folders/getFolder' -import type { ListFolderOptions, ListFolderResult } from '../folders/listFolder' -import type { MkdirOptions } from '../folders/addFolder' -import type { RenameFolderResult } from '../folders/updateFolder' -import type { DeleteFolderResult } from '../folders/deleteFolder' -import type { ListSharedFolderRow, ListSharedFoldersOptions } from '../sharedFolders/listSharedFolders' -import type { ListTeamRow, ListTeamsOptions } from '../teams/listTeams' -import type { TeamView } from '../teams/viewTeam' -import type { ListUserRow, ListUsersOptions } from '../users/userTypes' -import type { UserView } from '../users/userTypes' -import type { RecordShareInfo } from '../sharing/Sharing' -import type { VaultSummary } from '../vault/KeeperVault' - -export type CliResult = { - code: number - out: string - err: string - /** Set when the host UI must prompt for a masked password (never on the CLI line). */ - needPassword?: boolean - loginUsername?: string -} - -export type ParsedCli = { - positional: string[] - opts: Map -} - -/** - * Vault surface for CLI handlers. Methods beyond session/sync/records are optional; - * commands call `ensureCapability` so thin hosts fail with a clear message. - */ -export type KeeperCliVault = { - readonly isLoggedIn: boolean - login(username: string, password: string): Promise - loginWithSessionToken(username: string, sessionToken: string): Promise - logout(): Promise - sync(): Promise - getRecords(): DRecord[] - getSharedFolders(): DSharedFolder[] - registerDevice(deviceToken: string, privateKey: string, options?: { username?: string }): Promise - restoreSession(input: SessionRestoreInput): Promise - getSummary?: () => VaultSummary - findRecord?: (uidOrTitle: string) => DRecord | undefined - findRecords?: (criteria: string) => DRecord[] - getRecordShareInfo?: (recordUid: string) => Promise - listSharedFolders?: (options?: ListSharedFoldersOptions) => ListSharedFolderRow[] - listFolder?: (options?: ListFolderOptions) => Promise - tree?: (options?: FolderTreeBuildOptions) => Promise - changeDirectory?: (path: string) => Promise - getCurrentFolderUid?: () => string | null - getWorkingFolderDisplayName?: () => string - getFolder?: (uidOrName: string, options?: GetFolderOptions) => Promise - mkdir?: (path: string, options?: MkdirOptions) => Promise<{ folderUid: string; success: boolean; message?: string }> - renameFolder?: (folderPath: string, newName: string) => Promise - rmdir?: (patterns: string[], options?: { force?: boolean }) => Promise - listTeams?: (options?: ListTeamsOptions) => Promise - viewTeam?: (identifier: string) => Promise - listUsers?: (options?: ListUsersOptions) => Promise - viewUser?: (identifier: string) => Promise -} - -/** Host adapter (browser shell, Node script, tests). `readTextFile` is optional. */ -export type KeeperCliHost = { - getVault(): KeeperCliVault - envString(name: string): string | undefined - formatError(context: string, err: unknown): string - readTextFile?: (path: string) => Promise - getAccountUsername?: () => Promise -} - -export type CliHelpDoc = { - title: string - synopsis?: string - description?: string - arguments?: string - options?: string - environment?: string - examples?: string - seeAlso?: string - note?: string -} - -export type CliCommandDefinition = { - name: string - order?: number - description: string - usage: string - aliases?: readonly string[] - subcommands?: readonly string[] - flagOptions?: readonly string[] - /** When set, options outside this set are rejected (`--help` / `-h` always allowed). */ - allowedOptions?: ReadonlySet - help: CliHelpDoc - run: (host: KeeperCliHost, parsed: ParsedCli) => Promise -} diff --git a/KeeperSdk/src/cli/utils.ts b/KeeperSdk/src/cli/utils.ts deleted file mode 100644 index 5db5dae4..00000000 --- a/KeeperSdk/src/cli/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function utf8ToBase64Url(s: string): string { - const bytes = new TextEncoder().encode(s) - let b64: string - if (typeof Buffer !== 'undefined') { - b64 = Buffer.from(bytes).toString('base64') - } else { - let bin = '' - for (let i = 0; i < bytes.length; i++) { - bin += String.fromCharCode(bytes[i]!) - } - b64 = globalThis.btoa(bin) - } - return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') -} - -export function recordUid(rec: { uid?: string }): string { - return rec.uid || '(unknown uid)' -} diff --git a/KeeperSdk/src/cli/vaultSurface.ts b/KeeperSdk/src/cli/vaultSurface.ts deleted file mode 100644 index 2c7e65ba..00000000 --- a/KeeperSdk/src/cli/vaultSurface.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** Shared footer for vault-related command help (SDK APIs not yet exposed as CLI). */ -export const KEEPER_VAULT_SURFACE = ` -KeeperVault (JavaScript SDK) — operations available in code (not all exposed as CLI yet): - - Session: login, loginWithSessionToken, logout, resumeSession, sync, disconnect, registerDevice - Records: getRecords, findRecord, findRecords, getRecordByUid, getRecordsByType, - addRecord, updateRecord, deleteRecord, moveRecord, getRecordHistory, - printRecords - Sharing: shareRecord, removeRecordShare, getRecordShareInfo - Folders: listFolder, changeDirectory, getFolder, mkdir, addFolder, updateFolder, - renameFolder, deleteFolder, rmdir, tree, getCurrentFolderUid - Shared folders: getSharedFolders, listSharedFolders, shareFolder, … - Teams / metadata: getTeams, getRecordMetadata, getSummary, … - -Utilities exported from @keeper-security/keeper-sdk-javascript include searchRecords, -formatRecord, getRecordTitle, getRecordPassword, getRecordLogin, shareRecord, … -See the SDK package for full APIs. -`.trim() From e27bf40476fdff08fdb267543c4f872beb432788 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 15 Jun 2026 09:58:33 +0530 Subject: [PATCH 21/23] removed cli references for init commit --- KeeperSdk/package-lock.json | 546 +++++++++++++++++++++++++++++++++--- KeeperSdk/package.json | 2 + KeeperSdk/src/api.ts | 40 --- keeperapi/package-lock.json | 1 + 4 files changed, 517 insertions(+), 72 deletions(-) diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 0c2c31f6..071875e6 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -9,7 +9,8 @@ "version": "1.1.0", "license": "ISC", "dependencies": { - "@keeper-security/keeperapi": "17.2.3", + "@keeper-security/keeperapi": "17.2.6", + "asmcrypto.js": "^2.3.2", "ts-node": "^10.7.0", "typescript": "^4.6.3" }, @@ -56,9 +57,9 @@ } }, "node_modules/@keeper-security/keeperapi": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.2.3.tgz", - "integrity": "sha512-RT1ZnvfonFrPuLij8mPso/1eyTzurm2ikJxydzOA7oLQlqNRcH2FRramCan0sN85U5RNnM5QS05NVaJq4K72pg==", + "version": "17.2.6", + "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.2.6.tgz", + "integrity": "sha512-SEPP2rYioDFBJcCcjq/U2pUw5KBq6ymz85PTs/Na77Jwno7JXjabzKYApq+L1zMql0fW5UKYVUgTrSz2KImtbQ==", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", @@ -66,50 +67,424 @@ "faye-websocket": "^0.11.3", "form-data": "^4.0.4", "node-rsa": "^1.0.8" + } + }, + "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" }, - "devDependencies": { - "@babel/preset-env": "^7.23.5", - "@babel/preset-typescript": "^7.23.3", - "@rollup/plugin-commonjs": "^22.0.1", - "@rollup/plugin-node-resolve": "^13.3.0", - "@types/jest": "^24.0.15", - "@types/node": "^20.9.1", - "jest": "^29.6.1", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.8.1", - "protobufjs": "^7.2.4", - "protobufjs-cli": "^1.1.1", - "rollup": "^2.79.2", - "rollup-plugin-typescript2": "^0.32.1", - "ts-jest": "^29.1.1", - "ts-node": "^8.10.2", - "typescript": "^4.0.1" + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@keeper-security/keeperapi": { - "resolved": "../keeperapi", - "link": true + "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.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", - "dev": true, + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", "bin": { @@ -122,11 +497,81 @@ "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 + } + } + }, "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", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -139,8 +584,45 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, "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.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "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 a0dcd9c4..fdb8f5f8 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -15,6 +15,8 @@ "build": "tsc", "clean": "rm -rf dist node_modules", "rebuild": "npm run clean && npm install && npm run build", + "rebuild:local": "npm run clean && npm install && npm run link-local && npm run build:keeperapi && npm run build", + "build:keeperapi": "npm run build --prefix ../keeperapi", "link-local": "npm link ../keeperapi", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts index def13c4e..77fa28e8 100644 --- a/KeeperSdk/src/api.ts +++ b/KeeperSdk/src/api.ts @@ -181,46 +181,6 @@ export type { AuthProvider, SharedFolderPermissionsInput } from './folders/Folde export { SharedFolderManager } from './sharedFolders/SharedFolderManager' -export { - dispatchCliLine, - dispatchKeeperCli, - ensureKeeperCliRegistry, - registerCliCommand, - registerCliAlias, - getCliCommand, - listCliCommands, - listCliCommandNames, - listCliCommandNamesForLoginState, - listCliCommandsForLoginState, - isAuthCliCommand, - listDocumentedCommands, - getDetailedHelpPage, - formatDetailedHelpForCommand, - tokenizeArguments, - parseCliArgs, - wantsCliHelp, - rejectUnknownOptions, - loginWithCredentials, - loginWithSessionToken, - runLoginCommand, - runLogoutCommand, - KeeperCliParser, - createKeeperCliParser, - getKeeperCliPromptPrefix, - BUILTIN_CLI_COMMANDS, - registerBuiltinCliCommands, - listCommand, -} from './cli' -export type { KeeperCliParserOptions } from './cli' -export type { - CliResult, - ParsedCli, - CliCommandDefinition, - CliHelpDoc, - KeeperCliHost, - KeeperCliVault, -} from './cli' - export { Auth, KeeperEnvironment, syncDown, Authentication } from '@keeper-security/keeperapi' export type { diff --git a/keeperapi/package-lock.json b/keeperapi/package-lock.json index 387c5be5..c59e9095 100644 --- a/keeperapi/package-lock.json +++ b/keeperapi/package-lock.json @@ -7741,6 +7741,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, From 3c249778d579e1328dd6e396ae0a80dd837aeff6 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 15 Jun 2026 10:38:27 +0530 Subject: [PATCH 22/23] formatting issues fixed --- KeeperSdk/README.md | 18 +- KeeperSdk/package-lock.json | 1248 ++++++------ KeeperSdk/package.json | 72 +- KeeperSdk/src/api.ts | 398 ++-- KeeperSdk/src/auth/ConsoleAuthUI.ts | 357 ++-- KeeperSdk/src/auth/ConsoleLogin.ts | 583 +++--- KeeperSdk/src/auth/SessionManager.ts | 460 +++-- KeeperSdk/src/auth/UnavailableAuthUI.ts | 37 +- KeeperSdk/src/auth/config.ts | 58 +- KeeperSdk/src/auth/node/FileConfigLoader.ts | 57 +- KeeperSdk/src/auth/sessionRestore.ts | 356 ++-- KeeperSdk/src/browser.ts | 8 +- KeeperSdk/src/folders/FolderManager.ts | 424 ++-- KeeperSdk/src/folders/addFolder.ts | 612 +++--- KeeperSdk/src/folders/changeDirectory.ts | 367 ++-- KeeperSdk/src/folders/deleteFolder.ts | 513 ++--- KeeperSdk/src/folders/folderHelpers.ts | 141 +- KeeperSdk/src/folders/folderTree.ts | 648 +++--- KeeperSdk/src/folders/getFolder.ts | 406 ++-- KeeperSdk/src/folders/listFolder.ts | 708 ++++--- KeeperSdk/src/folders/updateFolder.ts | 421 ++-- KeeperSdk/src/index.ts | 845 ++++---- KeeperSdk/src/platform/browser/platform.ts | 122 +- KeeperSdk/src/platform/index.ts | 20 +- KeeperSdk/src/platform/node/platform.ts | 66 +- KeeperSdk/src/platform/types.ts | 28 +- KeeperSdk/src/records/RecordOperations.ts | 998 ++++----- KeeperSdk/src/records/RecordUtils.ts | 572 +++--- KeeperSdk/src/records/Totp.ts | 179 +- KeeperSdk/src/records/listRecordsTable.ts | 150 +- KeeperSdk/src/roles/RoleManager.ts | 289 +-- KeeperSdk/src/roles/addRole.ts | 456 +++-- KeeperSdk/src/roles/deleteRole.ts | 172 +- KeeperSdk/src/roles/index.ts | 120 +- KeeperSdk/src/roles/listRoles.ts | 601 +++--- KeeperSdk/src/roles/roleTypes.ts | 75 +- KeeperSdk/src/roles/roleUtils.ts | 399 ++-- KeeperSdk/src/roles/updateRole.ts | 338 ++-- KeeperSdk/src/roles/viewRole.ts | 804 ++++---- .../src/sharedFolders/SharedFolderManager.ts | 95 +- .../src/sharedFolders/listSharedFolders.ts | 295 +-- KeeperSdk/src/sharedFolders/shareFolder.ts | 885 ++++---- KeeperSdk/src/sharing/Sharing.ts | 514 ++--- KeeperSdk/src/storage/InMemoryStorage.ts | 334 ++-- KeeperSdk/src/teams/TeamManager.ts | 250 +-- KeeperSdk/src/teams/addTeam.ts | 857 ++++---- KeeperSdk/src/teams/deleteTeam.ts | 271 +-- KeeperSdk/src/teams/enterpriseData.ts | 1304 ++++++------ KeeperSdk/src/teams/listTeams.ts | 886 ++++---- KeeperSdk/src/teams/shareTeams.ts | 133 +- KeeperSdk/src/teams/teamUtils.ts | 396 ++-- KeeperSdk/src/teams/updateTeam.ts | 406 ++-- KeeperSdk/src/teams/viewTeam.ts | 445 +++-- KeeperSdk/src/users/UserManager.ts | 344 ++-- KeeperSdk/src/users/actionUser.ts | 649 +++--- KeeperSdk/src/users/addUser.ts | 544 ++--- KeeperSdk/src/users/aliasUser.ts | 249 ++- KeeperSdk/src/users/deleteUser.ts | 241 ++- KeeperSdk/src/users/listUsers.ts | 871 ++++---- KeeperSdk/src/users/teamUser.ts | 1351 +++++++------ KeeperSdk/src/users/updateUser.ts | 638 +++--- KeeperSdk/src/users/userTypes.ts | 559 +++--- KeeperSdk/src/users/viewUser.ts | 579 +++--- KeeperSdk/src/utils/Logger.ts | 96 +- KeeperSdk/src/utils/constants.ts | 276 +-- KeeperSdk/src/utils/errors.ts | 176 +- KeeperSdk/src/utils/guards.ts | 12 +- KeeperSdk/src/utils/index.ts | 72 +- KeeperSdk/src/utils/patterns.ts | 35 +- KeeperSdk/src/utils/types.ts | 10 +- KeeperSdk/src/vault/KeeperVault.ts | 1778 +++++++++-------- KeeperSdk/tsconfig.json | 28 +- 72 files changed, 16222 insertions(+), 13483 deletions(-) diff --git a/KeeperSdk/README.md b/KeeperSdk/README.md index 62fa48b8..bb49b99d 100644 --- a/KeeperSdk/README.md +++ b/KeeperSdk/README.md @@ -13,17 +13,21 @@ npm install @keeper-security/keeper-sdk-javascript ## Quickstart ```typescript -import { KeeperVault, ConsoleAuthUI, FileConfigLoader } from '@keeper-security/keeper-sdk-javascript' +import { + KeeperVault, + ConsoleAuthUI, + FileConfigLoader, +} from "@keeper-security/keeper-sdk-javascript"; const vault = new KeeperVault({ - authUI: new ConsoleAuthUI(), - configLoader: new FileConfigLoader('./keeper-config.json'), -}) + authUI: new ConsoleAuthUI(), + configLoader: new FileConfigLoader("./keeper-config.json"), +}); -await vault.login() -await vault.syncDown() +await vault.login(); +await vault.syncDown(); -console.log(`Loaded ${vault.records.size} records`) +console.log(`Loaded ${vault.records.size} records`); ``` ## Supported functionality diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 071875e6..9b27bd63 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -1,628 +1,628 @@ { - "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.1.0", - "license": "ISC", - "dependencies": { - "@keeper-security/keeperapi": "17.2.6", - "asmcrypto.js": "^2.3.2", - "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": "17.2.6", - "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.2.6.tgz", - "integrity": "sha512-SEPP2rYioDFBJcCcjq/U2pUw5KBq6ymz85PTs/Na77Jwno7JXjabzKYApq+L1zMql0fW5UKYVUgTrSz2KImtbQ==", - "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" - } - }, - "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.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", - "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "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.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", - "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 - } - } - }, - "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", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "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.5", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", - "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", - "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" - } + "name": "@keeper-security/keeper-sdk-javascript", + "version": "1.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@keeper-security/keeper-sdk-javascript", + "version": "1.1.0", + "license": "ISC", + "dependencies": { + "@keeper-security/keeperapi": "17.2.6", + "asmcrypto.js": "^2.3.2", + "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": "17.2.6", + "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.2.6.tgz", + "integrity": "sha512-SEPP2rYioDFBJcCcjq/U2pUw5KBq6ymz85PTs/Na77Jwno7JXjabzKYApq+L1zMql0fW5UKYVUgTrSz2KImtbQ==", + "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" + } + }, + "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.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "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 } + } + }, + "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", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "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.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "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 fdb8f5f8..eb2a00fc 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -1,38 +1,38 @@ { - "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.1.0", - "description": "High-level wrapper for Keeper Security JavaScript SDK", - "repository": { - "type": "git", - "url": "git+https://github.com/Keeper-Security/keeper-sdk-javascript.git", - "directory": "KeeperSdk" - }, - "license": "ISC", - "main": "dist/index.js", - "browser": "dist/browser.js", - "types": "dist/index.d.ts", - "scripts": { - "build": "tsc", - "clean": "rm -rf dist node_modules", - "rebuild": "npm run clean && npm install && npm run build", - "rebuild:local": "npm run clean && npm install && npm run link-local && npm run build:keeperapi && npm run build", - "build:keeperapi": "npm run build --prefix ../keeperapi", - "link-local": "npm link ../keeperapi", - "format": "prettier --write .", - "format:check": "prettier --check .", - "test": "jest", - "types": "tsc --watch", - "types:ci": "tsc", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@keeper-security/keeperapi": "17.2.6", - "ts-node": "^10.7.0", - "asmcrypto.js": "^2.3.2", - "typescript": "^4.6.3" - }, - "devDependencies": { - "@types/node": "^25.6.0", - "prettier": "^3.8.1" - } + "name": "@keeper-security/keeper-sdk-javascript", + "version": "1.1.0", + "description": "High-level wrapper for Keeper Security JavaScript SDK", + "repository": { + "type": "git", + "url": "git+https://github.com/Keeper-Security/keeper-sdk-javascript.git", + "directory": "KeeperSdk" + }, + "license": "ISC", + "main": "dist/index.js", + "browser": "dist/browser.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist node_modules", + "rebuild": "npm run clean && npm install && npm run build", + "rebuild:local": "npm run clean && npm install && npm run link-local && npm run build:keeperapi && npm run build", + "build:keeperapi": "npm run build --prefix ../keeperapi", + "link-local": "npm link ../keeperapi", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "jest", + "types": "tsc --watch", + "types:ci": "tsc", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@keeper-security/keeperapi": "17.2.6", + "ts-node": "^10.7.0", + "asmcrypto.js": "^2.3.2", + "typescript": "^4.6.3" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "prettier": "^3.8.1" + } } diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts index 77fa28e8..87c34774 100644 --- a/KeeperSdk/src/api.ts +++ b/KeeperSdk/src/api.ts @@ -1,201 +1,257 @@ -export { SessionManager } from './auth/SessionManager' +export { SessionManager } from "./auth/SessionManager"; export type { - KeeperJsonConfig, - ConfigLoader, - ConfigurationUser, - ConfigurationServerConfig, - ConfigurationDeviceConfig, -} from './auth/SessionManager' -export { connectSdkPlatform, getSdkPlatform, isSdkPlatformConnected } from './platform' -export type { SdkPlatform, SdkReadline, SdkRuntime } from './platform' + KeeperJsonConfig, + ConfigLoader, + ConfigurationUser, + ConfigurationServerConfig, + ConfigurationDeviceConfig, +} from "./auth/SessionManager"; +export { + connectSdkPlatform, + getSdkPlatform, + isSdkPlatformConnected, +} from "./platform"; +export type { SdkPlatform, SdkReadline, SdkRuntime } from "./platform"; -export { InMemoryStorage } from './storage/InMemoryStorage' +export { InMemoryStorage } from "./storage/InMemoryStorage"; export { - Logger, - ConsoleLogger, - LogLevel, - logger, - setLogger, - getLogger, - resetLogger, - KeeperSdkError, - isKeeperError, - extractErrorMessage, - extractResultCode, - SdkDefaults, - AuthDefaults, - ResultCodes, - KEEPER_PUBLIC_HOSTS, - isBoolean, - isString, - isNonEmptyString, - isNumber, - isObject, - anyIsBoolean, - EMAIL_PATTERN, - EMAIL_LIST_SEPARATOR_PATTERN, - isValidEmail, -} from './utils' -export type { ILogger, Nullable, Optional, DeepPartial, Immutable } from './utils' + Logger, + ConsoleLogger, + LogLevel, + logger, + setLogger, + getLogger, + resetLogger, + KeeperSdkError, + isKeeperError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + AuthDefaults, + ResultCodes, + KEEPER_PUBLIC_HOSTS, + isBoolean, + isString, + isNonEmptyString, + isNumber, + isObject, + anyIsBoolean, + EMAIL_PATTERN, + EMAIL_LIST_SEPARATOR_PATTERN, + isValidEmail, +} from "./utils"; +export type { + ILogger, + Nullable, + Optional, + DeepPartial, + Immutable, +} from "./utils"; export { - searchRecords, - formatRecord, - getRecordTitle, - getRecordType, - getRecordFields, - getRecordSummary, - getRecordPassword, - getRecordLogin, - getRecordUrl, - getRecordTotpUrl, - RecordVersion, -} from './records/RecordUtils' -export type { RecordSummary } from './records/RecordUtils' -export { parseTotpUrl, getTotpCode } from './records/Totp' -export type { TotpAlgorithm, TotpParams, TotpCode } from './records/Totp' -export { addRecord, updateRecord, deleteRecord, getRecordHistory, moveRecord } from './records/RecordOperations' + searchRecords, + formatRecord, + getRecordTitle, + getRecordType, + getRecordFields, + getRecordSummary, + getRecordPassword, + getRecordLogin, + getRecordUrl, + getRecordTotpUrl, + RecordVersion, +} from "./records/RecordUtils"; +export type { RecordSummary } from "./records/RecordUtils"; +export { parseTotpUrl, getTotpCode } from "./records/Totp"; +export type { TotpAlgorithm, TotpParams, TotpCode } from "./records/Totp"; +export { + addRecord, + updateRecord, + deleteRecord, + getRecordHistory, + moveRecord, +} from "./records/RecordOperations"; export type { - PasswordRecordData, - TypedRecordData, - RecordFieldInput, - NewRecordInput, - AddRecordResult, - UpdateRecordResult, - DeleteRecordResult, - HistoryEntry, - RecordHistoryResult, - MoveRecordInput, - MoveRecordResult, -} from './records/RecordOperations' - -export { shareRecord, removeRecordShare, getRecordShareInfo } from './sharing/Sharing' + PasswordRecordData, + TypedRecordData, + RecordFieldInput, + NewRecordInput, + AddRecordResult, + UpdateRecordResult, + DeleteRecordResult, + HistoryEntry, + RecordHistoryResult, + MoveRecordInput, + MoveRecordResult, +} from "./records/RecordOperations"; + +export { + shareRecord, + removeRecordShare, + getRecordShareInfo, +} from "./sharing/Sharing"; export type { - ShareRecordInput, - ShareRecordResult, - RemoveShareInput, - RemoveShareResult, - RecordShareInfo, - RecordUserPermission, - RecordSharedFolderPermission, -} from './sharing/Sharing' - -export { KeeperVault } from './vault/KeeperVault' -export type { KeeperVaultConfig, VaultSummary } from './vault/KeeperVault' - -export type { SessionRestoreInput } from './auth/sessionRestore' + ShareRecordInput, + ShareRecordResult, + RemoveShareInput, + RemoveShareResult, + RecordShareInfo, + RecordUserPermission, + RecordSharedFolderPermission, +} from "./sharing/Sharing"; + +export { KeeperVault } from "./vault/KeeperVault"; +export type { KeeperVaultConfig, VaultSummary } from "./vault/KeeperVault"; + +export type { SessionRestoreInput } from "./auth/sessionRestore"; export { - toSessionParams, - validateSessionRestoreInput, - sessionRestoreFromJson, - resolveSessionRestorePayload, -} from './auth/sessionRestore' + toSessionParams, + validateSessionRestoreInput, + sessionRestoreFromJson, + resolveSessionRestorePayload, +} from "./auth/sessionRestore"; -export { getFolder, findFolder, GetFolderFormat } from './folders/getFolder' +export { getFolder, findFolder, GetFolderFormat } from "./folders/getFolder"; export type { - GetFolderOptions, - GetFolderResult, - GetFolderResultFolder, - GetFolderResultSharedFolder, - GetFolderFormatInput, - FoundFolder, -} from './folders/getFolder' + GetFolderOptions, + GetFolderResult, + GetFolderResultFolder, + GetFolderResultSharedFolder, + GetFolderFormatInput, + FoundFolder, +} from "./folders/getFolder"; export { - FolderKind, - ParentFolderKind, - FolderObjectType, - FolderResultStatus, - DeleteResolution, - DeleteObjectType, - folderKindFromString, -} from './folders/folderHelpers' -export type { FolderKindOrLiteral } from './folders/folderHelpers' - -export { listFolder, findFolderUidByNameOrUid, listRootUserFolders } from './folders/listFolder' + FolderKind, + ParentFolderKind, + FolderObjectType, + FolderResultStatus, + DeleteResolution, + DeleteObjectType, + folderKindFromString, +} from "./folders/folderHelpers"; +export type { FolderKindOrLiteral } from "./folders/folderHelpers"; + +export { + listFolder, + findFolderUidByNameOrUid, + listRootUserFolders, +} from "./folders/listFolder"; export type { - ListFolderOptions, - ListFolderResult, - ListFolderFolderSimple, - ListFolderRecordSimple, - ListFolderFolderDetail, - ListFolderRecordDetail, -} from './folders/listFolder' + ListFolderOptions, + ListFolderResult, + ListFolderFolderSimple, + ListFolderRecordSimple, + ListFolderFolderDetail, + ListFolderRecordDetail, +} from "./folders/listFolder"; export { - listSharedFolders, - formatSharedFoldersTable, - renderSharedFoldersAsciiTable, -} from './sharedFolders/listSharedFolders' + listSharedFolders, + formatSharedFoldersTable, + renderSharedFoldersAsciiTable, +} from "./sharedFolders/listSharedFolders"; export type { - ListSharedFoldersOptions, - ListSharedFolderRow, - FormattedSharedFoldersTable, -} from './sharedFolders/listSharedFolders' + ListSharedFoldersOptions, + ListSharedFolderRow, + FormattedSharedFoldersTable, +} from "./sharedFolders/listSharedFolders"; -export { shareFolder, ShareFolderAction, ShareFolderUserResultStatus } from './sharedFolders/shareFolder' +export { + shareFolder, + ShareFolderAction, + ShareFolderUserResultStatus, +} from "./sharedFolders/shareFolder"; export type { - ShareFolderActionInput, - ShareFolderInput, - ShareFolderResult, - ShareFolderUserStatus, -} from './sharedFolders/shareFolder' + ShareFolderActionInput, + ShareFolderInput, + ShareFolderResult, + ShareFolderUserStatus, +} from "./sharedFolders/shareFolder"; export { - changeDirectory, - createVaultFolderSession, - tryResolvePath, - resolveSingleFolder, - getWorkingFolderDisplayName, - findParentFolderUid, - splitPathComponents, -} from './folders/changeDirectory' -export type { VaultFolderSession, ChangeDirectoryResult, TryResolvePathResult } from './folders/changeDirectory' - -export { addFolder, mkdir } from './folders/addFolder' -export type { AddFolderInput, AddFolderResult, MkdirOptions } from './folders/addFolder' - -export { updateFolder, renameFolder, updateSharedFolderPermissions } from './folders/updateFolder' -export type { UpdateFolderInput, UpdateFolderResult, RenameFolderResult } from './folders/updateFolder' + changeDirectory, + createVaultFolderSession, + tryResolvePath, + resolveSingleFolder, + getWorkingFolderDisplayName, + findParentFolderUid, + splitPathComponents, +} from "./folders/changeDirectory"; +export type { + VaultFolderSession, + ChangeDirectoryResult, + TryResolvePathResult, +} from "./folders/changeDirectory"; + +export { addFolder, mkdir } from "./folders/addFolder"; +export type { + AddFolderInput, + AddFolderResult, + MkdirOptions, +} from "./folders/addFolder"; export { - deleteFolder, - rmdir, - resolveRmdirPatternsToFolderUids, - buildFolderDeleteObject, -} from './folders/deleteFolder' -export type { DeleteFolderResult, RmdirOptions } from './folders/deleteFolder' + updateFolder, + renameFolder, + updateSharedFolderPermissions, +} from "./folders/updateFolder"; +export type { + UpdateFolderInput, + UpdateFolderResult, + RenameFolderResult, +} from "./folders/updateFolder"; export { - buildFolderTree, - renderFolderTreeAscii, - folderTreeAscii, - userPermissionToText, - recordPermissionToText, -} from './folders/folderTree' -export type { FolderTreeBuildOptions, FolderTreeNode, FolderTreeResult } from './folders/folderTree' + deleteFolder, + rmdir, + resolveRmdirPatternsToFolderUids, + buildFolderDeleteObject, +} from "./folders/deleteFolder"; +export type { DeleteFolderResult, RmdirOptions } from "./folders/deleteFolder"; -export { FolderManager } from './folders/FolderManager' -export type { AuthProvider, SharedFolderPermissionsInput } from './folders/FolderManager' +export { + buildFolderTree, + renderFolderTreeAscii, + folderTreeAscii, + userPermissionToText, + recordPermissionToText, +} from "./folders/folderTree"; +export type { + FolderTreeBuildOptions, + FolderTreeNode, + FolderTreeResult, +} from "./folders/folderTree"; + +export { FolderManager } from "./folders/FolderManager"; +export type { + AuthProvider, + SharedFolderPermissionsInput, +} from "./folders/FolderManager"; -export { SharedFolderManager } from './sharedFolders/SharedFolderManager' +export { SharedFolderManager } from "./sharedFolders/SharedFolderManager"; -export { Auth, KeeperEnvironment, syncDown, Authentication } from '@keeper-security/keeperapi' +export { + Auth, + KeeperEnvironment, + syncDown, + Authentication, +} from "@keeper-security/keeperapi"; export type { - DRecord, - DRecordMetadata, - DSharedFolder, - DTeam, - DUserFolder, - VaultStorage, - SyncResult, - SyncDownOptions, - ClientConfiguration, - DeviceConfig, - SessionStorage, - AuthUI3, - KeeperError, - LoginError, -} from '@keeper-security/keeperapi' + DRecord, + DRecordMetadata, + DSharedFolder, + DTeam, + DUserFolder, + VaultStorage, + SyncResult, + SyncDownOptions, + ClientConfiguration, + DeviceConfig, + SessionStorage, + AuthUI3, + KeeperError, + LoginError, +} from "@keeper-security/keeperapi"; diff --git a/KeeperSdk/src/auth/ConsoleAuthUI.ts b/KeeperSdk/src/auth/ConsoleAuthUI.ts index e4f8cb89..a0a79965 100644 --- a/KeeperSdk/src/auth/ConsoleAuthUI.ts +++ b/KeeperSdk/src/auth/ConsoleAuthUI.ts @@ -1,169 +1,208 @@ -import type { AuthUI3, DeviceApprovalChannel, TwoFactorChannelData } from '@keeper-security/keeperapi' -import { Authentication } from '@keeper-security/keeperapi' -import { getSdkPlatform } from '../platform' -import { logger, extractErrorMessage, KeeperSdkError, AuthDefaults, ResultCodes } from '../utils' +import type { + AuthUI3, + DeviceApprovalChannel, + TwoFactorChannelData, +} from "@keeper-security/keeperapi"; +import { Authentication } from "@keeper-security/keeperapi"; +import { getSdkPlatform } from "../platform"; +import { + logger, + extractErrorMessage, + KeeperSdkError, + AuthDefaults, + ResultCodes, +} from "../utils"; export class ConsoleAuthUI implements AuthUI3 { - private static readonly DEVICE_VERIFICATION = { - Email: 0, - KeeperPush: 1, - TFA: 2, - AdminApproval: 3, - } as const - - private static channelName(channel: number): string { - switch (channel) { - case ConsoleAuthUI.DEVICE_VERIFICATION.Email: - return 'Email Verification' - case ConsoleAuthUI.DEVICE_VERIFICATION.KeeperPush: - return 'Keeper Push' - case ConsoleAuthUI.DEVICE_VERIFICATION.TFA: - return 'Two-Factor Authentication' - case ConsoleAuthUI.DEVICE_VERIFICATION.AdminApproval: - return 'Admin Approval' - default: - return `Channel ${channel}` - } + private static readonly DEVICE_VERIFICATION = { + Email: 0, + KeeperPush: 1, + TFA: 2, + AdminApproval: 3, + } as const; + + private static channelName(channel: number): string { + switch (channel) { + case ConsoleAuthUI.DEVICE_VERIFICATION.Email: + return "Email Verification"; + case ConsoleAuthUI.DEVICE_VERIFICATION.KeeperPush: + return "Keeper Push"; + case ConsoleAuthUI.DEVICE_VERIFICATION.TFA: + return "Two-Factor Authentication"; + case ConsoleAuthUI.DEVICE_VERIFICATION.AdminApproval: + return "Admin Approval"; + default: + return `Channel ${channel}`; } - - private static twoFactorChannelName(channelType: Authentication.TwoFactorChannelType): string { - switch (channelType) { - case Authentication.TwoFactorChannelType.TWO_FA_CT_TOTP: - return 'Authenticator App (TOTP)' - case Authentication.TwoFactorChannelType.TWO_FA_CT_SMS: - return 'SMS' - case Authentication.TwoFactorChannelType.TWO_FA_CT_DUO: - return 'Duo Security' - case Authentication.TwoFactorChannelType.TWO_FA_CT_RSA: - return 'RSA SecurID' - case Authentication.TwoFactorChannelType.TWO_FA_CT_DNA: - return 'Keeper DNA' - case Authentication.TwoFactorChannelType.TWO_FA_CT_U2F: - return 'U2F Security Key' - case Authentication.TwoFactorChannelType.TWO_FA_CT_WEBAUTHN: - return 'WebAuthn Security Key' - case Authentication.TwoFactorChannelType.TWO_FA_CT_KEEPER: - return 'Keeper' - default: - throw new KeeperSdkError( - `Unsupported 2FA channel type: ${channelType}`, - ResultCodes.UNSUPPORTED_2FA_CHANNEL - ) - } + } + + private static twoFactorChannelName( + channelType: Authentication.TwoFactorChannelType, + ): string { + switch (channelType) { + case Authentication.TwoFactorChannelType.TWO_FA_CT_TOTP: + return "Authenticator App (TOTP)"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_SMS: + return "SMS"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_DUO: + return "Duo Security"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_RSA: + return "RSA SecurID"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_DNA: + return "Keeper DNA"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_U2F: + return "U2F Security Key"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_WEBAUTHN: + return "WebAuthn Security Key"; + case Authentication.TwoFactorChannelType.TWO_FA_CT_KEEPER: + return "Keeper"; + default: + throw new KeeperSdkError( + `Unsupported 2FA channel type: ${channelType}`, + ResultCodes.UNSUPPORTED_2FA_CHANNEL, + ); } - - private static async waitWithCancel(timeoutMs: number, cancel?: Promise): Promise { - const platform = getSdkPlatform() - if (!cancel) { - await platform.delay(timeoutMs) - return - } - await Promise.race([platform.delay(timeoutMs), cancel]) + } + + private static async waitWithCancel( + timeoutMs: number, + cancel?: Promise, + ): Promise { + const platform = getSdkPlatform(); + if (!cancel) { + await platform.delay(timeoutMs); + return; } - - public async waitForDeviceApproval(channels: DeviceApprovalChannel[], _isCloud: boolean): Promise { - const rl = getSdkPlatform().createReadline( - typeof process !== 'undefined' ? process.stdin : undefined, - typeof process !== 'undefined' ? process.stdout : undefined - ) - - try { - logger.info('\n--- Device Approval Required ---') - logger.info('This device needs to be approved before you can log in.') - logger.info('Available verification methods:') - channels.forEach((ch, i) => { - logger.info(` ${i + 1}. ${ConsoleAuthUI.channelName(ch.channel)}`) - }) - - const choice = (await rl.question('\nSelect method (number): ')).trim() - const idx = parseInt(choice, 10) - 1 - - if (isNaN(idx) || idx < 0 || idx >= channels.length) { - logger.warn('Invalid selection, cancelling.') - return false - } - - const selected = channels[idx] - logger.info(`\nSending ${ConsoleAuthUI.channelName(selected.channel)} request...`) - await selected.sendApprovalRequest() - - if (selected.validateCode) { - const code = (await rl.question('Enter verification code: ')).trim() - if (!code) return false - await selected.validateCode(code) - } else { - logger.info('Approval request sent. Waiting for approval on your other device...') - await ConsoleAuthUI.waitWithCancel(AuthDefaults.APPROVAL_TIMEOUT_MS) - } - - return true - } catch (e) { - logger.error('Device approval error:', extractErrorMessage(e)) - return false - } finally { - rl.close() - } + await Promise.race([platform.delay(timeoutMs), cancel]); + } + + public async waitForDeviceApproval( + channels: DeviceApprovalChannel[], + _isCloud: boolean, + ): Promise { + const rl = getSdkPlatform().createReadline( + typeof process !== "undefined" ? process.stdin : undefined, + typeof process !== "undefined" ? process.stdout : undefined, + ); + + try { + logger.info("\n--- Device Approval Required ---"); + logger.info("This device needs to be approved before you can log in."); + logger.info("Available verification methods:"); + channels.forEach((ch, i) => { + logger.info(` ${i + 1}. ${ConsoleAuthUI.channelName(ch.channel)}`); + }); + + const choice = (await rl.question("\nSelect method (number): ")).trim(); + const idx = parseInt(choice, 10) - 1; + + if (isNaN(idx) || idx < 0 || idx >= channels.length) { + logger.warn("Invalid selection, cancelling."); + return false; + } + + const selected = channels[idx]; + logger.info( + `\nSending ${ConsoleAuthUI.channelName(selected.channel)} request...`, + ); + await selected.sendApprovalRequest(); + + if (selected.validateCode) { + const code = (await rl.question("Enter verification code: ")).trim(); + if (!code) return false; + await selected.validateCode(code); + } else { + logger.info( + "Approval request sent. Waiting for approval on your other device...", + ); + await ConsoleAuthUI.waitWithCancel(AuthDefaults.APPROVAL_TIMEOUT_MS); + } + + return true; + } catch (e) { + logger.error("Device approval error:", extractErrorMessage(e)); + return false; + } finally { + rl.close(); } - - public async waitForTwoFactorCode(channels: TwoFactorChannelData[], cancel: Promise): Promise { - const rl = getSdkPlatform().createReadline( - typeof process !== 'undefined' ? process.stdin : undefined, - typeof process !== 'undefined' ? process.stdout : undefined - ) - - try { - logger.info('\n--- Two-Factor Authentication Required ---') - logger.info('Available 2FA methods:') - channels.forEach((ch, i) => { - const name = ConsoleAuthUI.twoFactorChannelName(ch.channel.channelType!) - logger.info(` ${i + 1}. ${name}`) - }) - - const choice = (await rl.question('\nSelect method (number): ')).trim() - const idx = parseInt(choice, 10) - 1 - - if (isNaN(idx) || idx < 0 || idx >= channels.length) { - logger.warn('Invalid selection, cancelling.') - return false - } - - const selected = channels[idx] - const name = ConsoleAuthUI.twoFactorChannelName(selected.channel.channelType!) - - if (selected.availablePushes && selected.availablePushes.length > 0) { - const pushChoice = (await rl.question(`Send push notification for ${name}? (y/n): `)).trim() - if (pushChoice.toLowerCase() === 'y' && selected.sendPush) { - selected.sendPush(selected.availablePushes[0]) - logger.info('Push sent. Waiting for approval...') - await ConsoleAuthUI.waitWithCancel(AuthDefaults.APPROVAL_TIMEOUT_MS, cancel) - return true - } - } - - const code = (await rl.question(`Enter ${name} code: `)).trim() - if (!code) return false - - selected.sendCode(code) - await ConsoleAuthUI.waitWithCancel(AuthDefaults.CODE_VALIDATION_DELAY_MS, cancel) - return true - } catch (e) { - logger.error('2FA error:', extractErrorMessage(e)) - return false - } finally { - rl.close() + } + + public async waitForTwoFactorCode( + channels: TwoFactorChannelData[], + cancel: Promise, + ): Promise { + const rl = getSdkPlatform().createReadline( + typeof process !== "undefined" ? process.stdin : undefined, + typeof process !== "undefined" ? process.stdout : undefined, + ); + + try { + logger.info("\n--- Two-Factor Authentication Required ---"); + logger.info("Available 2FA methods:"); + channels.forEach((ch, i) => { + const name = ConsoleAuthUI.twoFactorChannelName( + ch.channel.channelType!, + ); + logger.info(` ${i + 1}. ${name}`); + }); + + const choice = (await rl.question("\nSelect method (number): ")).trim(); + const idx = parseInt(choice, 10) - 1; + + if (isNaN(idx) || idx < 0 || idx >= channels.length) { + logger.warn("Invalid selection, cancelling."); + return false; + } + + const selected = channels[idx]; + const name = ConsoleAuthUI.twoFactorChannelName( + selected.channel.channelType!, + ); + + if (selected.availablePushes && selected.availablePushes.length > 0) { + const pushChoice = ( + await rl.question(`Send push notification for ${name}? (y/n): `) + ).trim(); + if (pushChoice.toLowerCase() === "y" && selected.sendPush) { + selected.sendPush(selected.availablePushes[0]); + logger.info("Push sent. Waiting for approval..."); + await ConsoleAuthUI.waitWithCancel( + AuthDefaults.APPROVAL_TIMEOUT_MS, + cancel, + ); + return true; } + } + + const code = (await rl.question(`Enter ${name} code: `)).trim(); + if (!code) return false; + + selected.sendCode(code); + await ConsoleAuthUI.waitWithCancel( + AuthDefaults.CODE_VALIDATION_DELAY_MS, + cancel, + ); + return true; + } catch (e) { + logger.error("2FA error:", extractErrorMessage(e)); + return false; + } finally { + rl.close(); } - - public async getPassword(isAlternate: boolean): Promise { - const rl = getSdkPlatform().createReadline( - typeof process !== 'undefined' ? process.stdin : undefined, - typeof process !== 'undefined' ? process.stdout : undefined - ) - try { - const label = isAlternate ? 'alternate master password' : 'master password' - return (await rl.question(`Enter your ${label}: `)).trim() - } finally { - rl.close() - } + } + + public async getPassword(isAlternate: boolean): Promise { + const rl = getSdkPlatform().createReadline( + typeof process !== "undefined" ? process.stdin : undefined, + typeof process !== "undefined" ? process.stdout : undefined, + ); + try { + const label = isAlternate + ? "alternate master password" + : "master password"; + return (await rl.question(`Enter your ${label}: `)).trim(); + } finally { + rl.close(); } + } } diff --git a/KeeperSdk/src/auth/ConsoleLogin.ts b/KeeperSdk/src/auth/ConsoleLogin.ts index 93f30bba..7ca0a915 100644 --- a/KeeperSdk/src/auth/ConsoleLogin.ts +++ b/KeeperSdk/src/auth/ConsoleLogin.ts @@ -1,346 +1,377 @@ -import type { AuthUI3 } from '@keeper-security/keeperapi' -import { KeeperVault } from '../vault/KeeperVault' -import { getSdkPlatform } from '../platform' -import type { SdkReadline } from '../platform' +import type { AuthUI3 } from "@keeper-security/keeperapi"; +import { KeeperVault } from "../vault/KeeperVault"; +import { getSdkPlatform } from "../platform"; +import type { SdkReadline } from "../platform"; import { - logger, - extractResultCode, - extractErrorMessage, - KeeperSdkError, - SdkDefaults, - AuthDefaults, - ResultCodes, - KEEPER_PUBLIC_HOSTS, -} from '../utils' -import { ConsoleAuthUI } from './ConsoleAuthUI' -import type { KeeperJsonConfig } from './config' - -const DEFAULT_REGION = 'US' -const MASK_CHAR = '*' -const NOOP_WRITE = (() => true) as typeof process.stdout.write + logger, + extractResultCode, + extractErrorMessage, + KeeperSdkError, + SdkDefaults, + AuthDefaults, + ResultCodes, + KEEPER_PUBLIC_HOSTS, +} from "../utils"; +import { ConsoleAuthUI } from "./ConsoleAuthUI"; +import type { KeeperJsonConfig } from "./config"; + +const DEFAULT_REGION = "US"; +const MASK_CHAR = "*"; +const NOOP_WRITE = (() => true) as typeof process.stdout.write; enum CliCharAction { - Submit, - Cancel, - Backspace, - Append, + Submit, + Cancel, + Backspace, + Append, } type ConsoleHandlers = { - log: typeof console.log - warn: typeof console.warn - debug: typeof console.debug - error: typeof console.error - stdoutWrite: typeof process.stdout.write - stderrWrite: typeof process.stderr.write -} - -let rlManager: ReadlineManager | null = null -let suppressionDepth = 0 -let originals: ConsoleHandlers | null = null + log: typeof console.log; + warn: typeof console.warn; + debug: typeof console.debug; + error: typeof console.error; + stdoutWrite: typeof process.stdout.write; + stderrWrite: typeof process.stderr.write; +}; + +let rlManager: ReadlineManager | null = null; +let suppressionDepth = 0; +let originals: ConsoleHandlers | null = null; function captureConsoleHandlers(): ConsoleHandlers { - return { - log: console.log, - warn: console.warn, - debug: console.debug, - error: console.error, - stdoutWrite: process.stdout.write.bind(process.stdout), - stderrWrite: process.stderr.write.bind(process.stderr), - } + return { + log: console.log, + warn: console.warn, + debug: console.debug, + error: console.error, + stdoutWrite: process.stdout.write.bind(process.stdout), + stderrWrite: process.stderr.write.bind(process.stderr), + }; } function applyConsoleHandlers(h: ConsoleHandlers): void { - console.log = h.log - console.warn = h.warn - console.debug = h.debug - console.error = h.error - process.stdout.write = h.stdoutWrite - process.stderr.write = h.stderrWrite + console.log = h.log; + console.warn = h.warn; + console.debug = h.debug; + console.error = h.error; + process.stdout.write = h.stdoutWrite; + process.stderr.write = h.stderrWrite; } function classifyInputChar(ch: string): CliCharAction { - if (ch === '\n' || ch === '\r') return CliCharAction.Submit - if (ch === '\u0003') return CliCharAction.Cancel - if (ch === '\u007F' || ch === '\b') return CliCharAction.Backspace - return CliCharAction.Append + if (ch === "\n" || ch === "\r") return CliCharAction.Submit; + if (ch === "\u0003") return CliCharAction.Cancel; + if (ch === "\u007F" || ch === "\b") return CliCharAction.Backspace; + return CliCharAction.Append; } class ReadlineManager { - private rl: SdkReadline | null = null + private rl: SdkReadline | null = null; - private getOrCreate(): SdkReadline { - if (!this.rl) { - this.rl = getSdkPlatform().createReadline(process.stdin, process.stdout) - } - return this.rl + private getOrCreate(): SdkReadline { + if (!this.rl) { + this.rl = getSdkPlatform().createReadline(process.stdin, process.stdout); } - - public async question(query: string): Promise { - const rl = this.getOrCreate() - const answer = await rl.question(query) - return answer.trim() - } - - public close(): void { - if (this.rl) { - this.rl.close() - this.rl = null - } + return this.rl; + } + + public async question(query: string): Promise { + const rl = this.getOrCreate(); + const answer = await rl.question(query); + return answer.trim(); + } + + public close(): void { + if (this.rl) { + this.rl.close(); + this.rl = null; } + } } function getReadlineManager(): ReadlineManager { - if (!rlManager) { - rlManager = new ReadlineManager() - } - return rlManager + if (!rlManager) { + rlManager = new ReadlineManager(); + } + return rlManager; } export function prompt(question: string, masked = false): Promise { - const mgr = getReadlineManager() - if (!masked) { - return mgr.question(question) + const mgr = getReadlineManager(); + if (!masked) { + return mgr.question(question); + } + + return new Promise((resolve, reject) => { + mgr.close(); + process.stdout.write(question); + let buf = ""; + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.setEncoding("utf8"); + + function exitRawMode() { + process.stdout.write("\n"); + process.stdin.setRawMode(false); + process.stdin.pause(); + process.stdin.removeListener("data", onData); } - return new Promise((resolve, reject) => { - mgr.close() - process.stdout.write(question) - let buf = '' - process.stdin.setRawMode(true) - process.stdin.resume() - process.stdin.setEncoding('utf8') - - function exitRawMode() { - process.stdout.write('\n') - process.stdin.setRawMode(false) - process.stdin.pause() - process.stdin.removeListener('data', onData) - } - - const onData = (str: string) => { - for (const ch of str) { - switch (classifyInputChar(ch)) { - case CliCharAction.Submit: - exitRawMode() - resolve(buf.trim()) - return - case CliCharAction.Cancel: - exitRawMode() - reject(new KeeperSdkError('Operation cancelled by user.', ResultCodes.USER_CANCELLED)) - return - case CliCharAction.Backspace: - if (buf.length > 0) { - buf = buf.slice(0, -1) - process.stdout.write('\b \b') - } - break - case CliCharAction.Append: - buf += ch - process.stdout.write(MASK_CHAR) - break - } + const onData = (str: string) => { + for (const ch of str) { + switch (classifyInputChar(ch)) { + case CliCharAction.Submit: + exitRawMode(); + resolve(buf.trim()); + return; + case CliCharAction.Cancel: + exitRawMode(); + reject( + new KeeperSdkError( + "Operation cancelled by user.", + ResultCodes.USER_CANCELLED, + ), + ); + return; + case CliCharAction.Backspace: + if (buf.length > 0) { + buf = buf.slice(0, -1); + process.stdout.write("\b \b"); } + break; + case CliCharAction.Append: + buf += ch; + process.stdout.write(MASK_CHAR); + break; } + } + }; - process.stdin.on('data', onData) - }) + process.stdin.on("data", onData); + }); } -export async function loadKeeperConfig(preloaded?: KeeperJsonConfig): Promise { - if (preloaded) return preloaded - return getSdkPlatform().createFileConfigLoader().load() +export async function loadKeeperConfig( + preloaded?: KeeperJsonConfig, +): Promise { + if (preloaded) return preloaded; + return getSdkPlatform().createFileConfigLoader().load(); } -export async function resolveServer(username?: string, preloadedConfig?: KeeperJsonConfig): Promise { - const config = await loadKeeperConfig(preloadedConfig) - const configServer = config.last_server || config.server - - if (username) { - const users = config.users || [] - const userEntry = users.find((u) => u.user?.toLowerCase() === username.toLowerCase()) - if (userEntry?.server) return userEntry.server - } - - if (configServer) return configServer - - logger.info('Select server region:') - const entries = Object.entries(KEEPER_PUBLIC_HOSTS) - entries.forEach(([region, host], i) => { - logger.info(` ${i + 1}. ${region} (${host})`) - }) - logger.info(` Or enter a hostname directly (e.g. dev.keepersecurity.com)`) - - const choice = await prompt(`Region [1 = ${DEFAULT_REGION}]: `) - if (!choice) return KEEPER_PUBLIC_HOSTS[DEFAULT_REGION] - - const idx = parseInt(choice, 10) - 1 - if (idx >= 0 && idx < entries.length) return entries[idx][1] - - return KEEPER_PUBLIC_HOSTS[choice.toUpperCase()] || choice +export async function resolveServer( + username?: string, + preloadedConfig?: KeeperJsonConfig, +): Promise { + const config = await loadKeeperConfig(preloadedConfig); + const configServer = config.last_server || config.server; + + if (username) { + const users = config.users || []; + const userEntry = users.find( + (u) => u.user?.toLowerCase() === username.toLowerCase(), + ); + if (userEntry?.server) return userEntry.server; + } + + if (configServer) return configServer; + + logger.info("Select server region:"); + const entries = Object.entries(KEEPER_PUBLIC_HOSTS); + entries.forEach(([region, host], i) => { + logger.info(` ${i + 1}. ${region} (${host})`); + }); + logger.info(` Or enter a hostname directly (e.g. dev.keepersecurity.com)`); + + const choice = await prompt(`Region [1 = ${DEFAULT_REGION}]: `); + if (!choice) return KEEPER_PUBLIC_HOSTS[DEFAULT_REGION]; + + const idx = parseInt(choice, 10) - 1; + if (idx >= 0 && idx < entries.length) return entries[idx][1]; + + return KEEPER_PUBLIC_HOSTS[choice.toUpperCase()] || choice; } export function suppressLogs(): () => void { - if (suppressionDepth === 0) { - originals = captureConsoleHandlers() - applyConsoleHandlers({ - log: () => {}, - warn: () => {}, - debug: () => {}, - error: () => {}, - stdoutWrite: NOOP_WRITE, - stderrWrite: NOOP_WRITE, - }) - } - suppressionDepth++ - - let restored = false - return () => { - if (restored) return - restored = true - suppressionDepth-- - if (suppressionDepth === 0 && originals) { - applyConsoleHandlers(originals) - originals = null - } + if (suppressionDepth === 0) { + originals = captureConsoleHandlers(); + applyConsoleHandlers({ + log: () => {}, + warn: () => {}, + debug: () => {}, + error: () => {}, + stdoutWrite: NOOP_WRITE, + stderrWrite: NOOP_WRITE, + }); + } + suppressionDepth++; + + let restored = false; + return () => { + if (restored) return; + restored = true; + suppressionDepth--; + if (suppressionDepth === 0 && originals) { + applyConsoleHandlers(originals); + originals = null; } + }; } async function withSuppressedOutput(fn: () => Promise): Promise { - const restore = suppressLogs() - try { - return await fn() - } finally { - restore() - } + const restore = suppressLogs(); + try { + return await fn(); + } finally { + restore(); + } } function unsuppressLogs(): () => void { - if (suppressionDepth === 0 || !originals) return () => {} + if (suppressionDepth === 0 || !originals) return () => {}; - const overrides = captureConsoleHandlers() - applyConsoleHandlers(originals) + const overrides = captureConsoleHandlers(); + applyConsoleHandlers(originals); - let restored = false - return () => { - if (restored) return - restored = true - applyConsoleHandlers(overrides) - } + let restored = false; + return () => { + if (restored) return; + restored = true; + applyConsoleHandlers(overrides); + }; } function unsuppressedAuthUI(): AuthUI3 { - const ui = new ConsoleAuthUI() - const wrap = (fn: (...args: A) => Promise) => - async (...args: A): Promise => { - const restore = unsuppressLogs() - try { - return await fn(...args) - } finally { - restore() - } - } - return { - waitForDeviceApproval: wrap(ui.waitForDeviceApproval.bind(ui)), - waitForTwoFactorCode: wrap(ui.waitForTwoFactorCode.bind(ui)), - getPassword: wrap(ui.getPassword.bind(ui)), - } + const ui = new ConsoleAuthUI(); + const wrap = + (fn: (...args: A) => Promise) => + async (...args: A): Promise => { + const restore = unsuppressLogs(); + try { + return await fn(...args); + } finally { + restore(); + } + }; + return { + waitForDeviceApproval: wrap(ui.waitForDeviceApproval.bind(ui)), + waitForTwoFactorCode: wrap(ui.waitForTwoFactorCode.bind(ui)), + getPassword: wrap(ui.getPassword.bind(ui)), + }; } export async function login(): Promise { - const config = await loadKeeperConfig() - const defaultUsername = config.last_login || config.user || '' - - const host = defaultUsername ? await resolveServer(defaultUsername, config) : undefined + const config = await loadKeeperConfig(); + const defaultUsername = config.last_login || config.user || ""; + + const host = defaultUsername + ? await resolveServer(defaultUsername, config) + : undefined; + + if (defaultUsername && host) { + const vault = await tryPersistentLogin(host, defaultUsername); + if (vault) return vault; + } + + let username: string; + if (defaultUsername) { + logger.info(`Enter master password for ${defaultUsername}`); + username = defaultUsername; + } else { + username = await prompt("Username (email): "); + } + + if (!username) { + throw new KeeperSdkError( + "Username is required.", + ResultCodes.MISSING_USERNAME, + ); + } - if (defaultUsername && host) { - const vault = await tryPersistentLogin(host, defaultUsername) - if (vault) return vault - } + const resolvedHost = host || (await resolveServer(username, config)); + return await interactiveLogin(resolvedHost, username); +} - let username: string - if (defaultUsername) { - logger.info(`Enter master password for ${defaultUsername}`) - username = defaultUsername - } else { - username = await prompt('Username (email): ') - } +async function tryPersistentLogin( + host: string, + username: string, +): Promise { + const vault = new KeeperVault({ + host, + clientVersion: SdkDefaults.CLIENT_VERSION, + authUI: unsuppressedAuthUI(), + }); + try { + await withSuppressedOutput(() => vault.resumeSession()); + logger.info(`Logging in to Keeper as ${username}`); + logger.info("Successfully authenticated with Persistent Login"); + return await syncVault(vault); + } catch (err) { + logger.debug("Persistent login failed:", extractErrorMessage(err)); + vault.disconnect(); + return null; + } +} - if (!username) { - throw new KeeperSdkError('Username is required.', ResultCodes.MISSING_USERNAME) +async function interactiveLogin( + host: string, + username: string, +): Promise { + const vault = new KeeperVault({ + host, + clientVersion: SdkDefaults.CLIENT_VERSION, + authUI: unsuppressedAuthUI(), + }); + + for (let attempt = 1; attempt <= AuthDefaults.MAX_LOGIN_ATTEMPTS; attempt++) { + const password = await prompt("Password: ", true); + + if (!password) { + throw new KeeperSdkError( + "Password is required.", + ResultCodes.MISSING_PASSWORD, + ); } - const resolvedHost = host || (await resolveServer(username, config)) - return await interactiveLogin(resolvedHost, username) -} - -async function tryPersistentLogin(host: string, username: string): Promise { - const vault = new KeeperVault({ - host, - clientVersion: SdkDefaults.CLIENT_VERSION, - authUI: unsuppressedAuthUI(), - }) try { - await withSuppressedOutput(() => vault.resumeSession()) - logger.info(`Logging in to Keeper as ${username}`) - logger.info('Successfully authenticated with Persistent Login') - return await syncVault(vault) + await withSuppressedOutput(() => vault.login(username, password)); + logger.info("Successfully authenticated with Master Password\n"); + return await syncVault(vault); } catch (err) { - logger.debug('Persistent login failed:', extractErrorMessage(err)) - vault.disconnect() - return null - } -} - -async function interactiveLogin(host: string, username: string): Promise { - const vault = new KeeperVault({ - host, - clientVersion: SdkDefaults.CLIENT_VERSION, - authUI: unsuppressedAuthUI(), - }) - - for (let attempt = 1; attempt <= AuthDefaults.MAX_LOGIN_ATTEMPTS; attempt++) { - const password = await prompt('Password: ', true) - - if (!password) { - throw new KeeperSdkError('Password is required.', ResultCodes.MISSING_PASSWORD) - } - - try { - await withSuppressedOutput(() => vault.login(username, password)) - logger.info('Successfully authenticated with Master Password\n') - return await syncVault(vault) - } catch (err) { - const resultCode = extractResultCode(err) - if (resultCode === ResultCodes.INVALID_CREDENTIALS) { - const remaining = AuthDefaults.MAX_LOGIN_ATTEMPTS - attempt - if (remaining > 0) { - logger.warn(`Invalid credentials (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`) - continue - } - throw new KeeperSdkError( - `Maximum login attempts (${AuthDefaults.MAX_LOGIN_ATTEMPTS}) exceeded.`, - ResultCodes.MAX_ATTEMPTS_EXCEEDED - ) - } - throw KeeperSdkError.from(err) + const resultCode = extractResultCode(err); + if (resultCode === ResultCodes.INVALID_CREDENTIALS) { + const remaining = AuthDefaults.MAX_LOGIN_ATTEMPTS - attempt; + if (remaining > 0) { + logger.warn( + `Invalid credentials (${remaining} attempt${remaining === 1 ? "" : "s"} remaining)`, + ); + continue; } + throw new KeeperSdkError( + `Maximum login attempts (${AuthDefaults.MAX_LOGIN_ATTEMPTS}) exceeded.`, + ResultCodes.MAX_ATTEMPTS_EXCEEDED, + ); + } + throw KeeperSdkError.from(err); } + } - throw new KeeperSdkError( - `Maximum login attempts (${AuthDefaults.MAX_LOGIN_ATTEMPTS}) exceeded.`, - ResultCodes.MAX_ATTEMPTS_EXCEEDED - ) + throw new KeeperSdkError( + `Maximum login attempts (${AuthDefaults.MAX_LOGIN_ATTEMPTS}) exceeded.`, + ResultCodes.MAX_ATTEMPTS_EXCEEDED, + ); } async function syncVault(vault: KeeperVault): Promise { - logger.info('Syncing vault...') - await withSuppressedOutput(() => vault.sync()) - logger.info(`Vault synced. ${vault.getSummary().recordCount} records loaded.\n`) - return vault + logger.info("Syncing vault..."); + await withSuppressedOutput(() => vault.sync()); + logger.info( + `Vault synced. ${vault.getSummary().recordCount} records loaded.\n`, + ); + return vault; } export function cleanup(vault: KeeperVault): void { - vault.disconnect() - getReadlineManager().close() + vault.disconnect(); + getReadlineManager().close(); } diff --git a/KeeperSdk/src/auth/SessionManager.ts b/KeeperSdk/src/auth/SessionManager.ts index 0370b3ce..30a21b84 100644 --- a/KeeperSdk/src/auth/SessionManager.ts +++ b/KeeperSdk/src/auth/SessionManager.ts @@ -1,240 +1,274 @@ import { - normal64Bytes, - type DeviceConfig, - type SessionStorage, - type KeeperHost, - type SessionParams, -} from '@keeper-security/keeperapi' -import { getSdkPlatform } from '../platform' -import { logger, extractErrorMessage } from '../utils' -import type { Nullable } from '../utils' + normal64Bytes, + type DeviceConfig, + type SessionStorage, + type KeeperHost, + type SessionParams, +} from "@keeper-security/keeperapi"; +import { getSdkPlatform } from "../platform"; +import { logger, extractErrorMessage } from "../utils"; +import type { Nullable } from "../utils"; import type { - ConfigLoader, - KeeperJsonConfig, - ConfigurationServerConfig, - ConfigurationUser, -} from './config' -import { isValidKeeperConfig } from './config' + ConfigLoader, + KeeperJsonConfig, + ConfigurationServerConfig, + ConfigurationUser, +} from "./config"; +import { isValidKeeperConfig } from "./config"; export type { - KeeperJsonConfig, - ConfigLoader, - ConfigurationUser, - ConfigurationServerConfig, - ConfigurationDeviceConfig, -} from './config' + KeeperJsonConfig, + ConfigLoader, + ConfigurationUser, + ConfigurationServerConfig, + ConfigurationDeviceConfig, +} from "./config"; type ResolvedDevice = { - deviceToken: Uint8Array - privateKey: Uint8Array - serverInfo: Array> -} + deviceToken: Uint8Array; + privateKey: Uint8Array; + serverInfo: Array>; +}; type DeviceCacheEntry = { - username: string - device: Nullable -} + username: string; + device: Nullable; +}; export class SessionManager implements SessionStorage { - private readonly configLoader: ConfigLoader - private sessionParams: Nullable = null - private _lastUsername?: string - private _keeperConfig: Nullable = null - private _deviceCache: Nullable = null - private sessionDevices = new Map() - private sessionCloneCodes = new Map() - - constructor(configDir?: string) - constructor(loader: ConfigLoader) - constructor(configDirOrLoader?: string | ConfigLoader) { - if (typeof configDirOrLoader === 'string') { - this.configLoader = getSdkPlatform().createFileConfigLoader(configDirOrLoader) - } else if (configDirOrLoader === undefined) { - this.configLoader = getSdkPlatform().createFileConfigLoader() - } else { - this.configLoader = configDirOrLoader - } + private readonly configLoader: ConfigLoader; + private sessionParams: Nullable = null; + private _lastUsername?: string; + private _keeperConfig: Nullable = null; + private _deviceCache: Nullable = null; + private sessionDevices = new Map(); + private sessionCloneCodes = new Map(); + + constructor(configDir?: string); + constructor(loader: ConfigLoader); + constructor(configDirOrLoader?: string | ConfigLoader) { + if (typeof configDirOrLoader === "string") { + this.configLoader = + getSdkPlatform().createFileConfigLoader(configDirOrLoader); + } else if (configDirOrLoader === undefined) { + this.configLoader = getSdkPlatform().createFileConfigLoader(); + } else { + this.configLoader = configDirOrLoader; } - - public get configDir(): string { - return this.configLoader.configDir + } + + public get configDir(): string { + return this.configLoader.configDir; + } + + public get lastUsername(): string | undefined { + return this._lastUsername; + } + + public async getLastUsername(): Promise { + if (this._lastUsername) return this._lastUsername; + const keeperConfig = await this.loadKeeperConfig(); + return keeperConfig.last_login || keeperConfig.user || undefined; + } + + public async getDeviceConfig(host: string): Promise { + const username = await this.getLastUsername(); + if (username) { + const device = await this.findDeviceInKeeperConfig(username); + if (device) { + return { + deviceToken: device.deviceToken, + privateKey: device.privateKey, + }; + } } - public get lastUsername(): string | undefined { - return this._lastUsername + return this.sessionDevices.get(host) || {}; + } + + public createOnDeviceConfig( + host: string, + ): (deviceConfig: DeviceConfig) => Promise { + return async (deviceConfig: DeviceConfig) => { + this.sessionDevices.set(host, { ...deviceConfig }); + }; + } + + private cloneCodeKey(host: KeeperHost, username: string): string { + return `${host}::${username}`; + } + + public async getCloneCode( + host: KeeperHost, + username: string, + ): Promise> { + const hostStr = String(host); + + const key = this.cloneCodeKey(host, username); + const sessionCode = this.sessionCloneCodes.get(key); + if (sessionCode) return sessionCode; + + const device = await this.findDeviceInKeeperConfig(username); + if (device) { + const serverInfo = device.serverInfo.find( + (entry) => entry.server === hostStr, + ); + if (serverInfo) { + return normal64Bytes(serverInfo.clone_code); + } } - public async getLastUsername(): Promise { - if (this._lastUsername) return this._lastUsername - const keeperConfig = await this.loadKeeperConfig() - return keeperConfig.last_login || keeperConfig.user || undefined - } - - public async getDeviceConfig(host: string): Promise { - const username = await this.getLastUsername() - if (username) { - const device = await this.findDeviceInKeeperConfig(username) - if (device) { - return { - deviceToken: device.deviceToken, - privateKey: device.privateKey, - } - } + return null; + } + + public async saveCloneCode( + host: KeeperHost, + username: string, + cloneCode: Uint8Array, + ): Promise { + const key = this.cloneCodeKey(host, username); + this.sessionCloneCodes.set(key, cloneCode); + await this.updateKeeperConfigCloneCode(String(host), username, cloneCode); + } + + private async updateKeeperConfigCloneCode( + host: string, + username: string, + cloneCode: Uint8Array, + ): Promise { + try { + const parsed = await this.configLoader.load(); + if (!parsed || Object.keys(parsed).length === 0) return; + + let updated = false; + const encodedCloneCode = Buffer.from(cloneCode).toString("base64url"); + + const server = parsed.last_server || parsed.server; + if ( + parsed.user?.toLowerCase() === username.toLowerCase() && + server === host + ) { + parsed.clone_code = encodedCloneCode; + updated = true; + } + + const user = (parsed.users || []).find( + (configUser) => + configUser.user?.toLowerCase() === username.toLowerCase(), + ); + if (user?.last_device?.device_token) { + const device = (parsed.devices || []).find( + (configDevice) => + configDevice.device_token === user.last_device!.device_token, + ); + if (device?.server_info) { + const serverInfo = device.server_info.find( + (entry) => entry.server === host, + ); + if (serverInfo) { + serverInfo.clone_code = encodedCloneCode; + updated = true; + } } - - return this.sessionDevices.get(host) || {} + } + + if (updated) { + await this.configLoader.save(parsed); + this._keeperConfig = null; + this._deviceCache = null; + } + } catch (err) { + logger.warn( + "Failed to update keeper config clone code:", + extractErrorMessage(err), + ); } - - public createOnDeviceConfig(host: string): (deviceConfig: DeviceConfig) => Promise { - return async (deviceConfig: DeviceConfig) => { - this.sessionDevices.set(host, { ...deviceConfig }) - } + } + + public async getSessionParameters(): Promise> { + return this.sessionParams; + } + + public async saveSessionParameters( + params: Partial, + ): Promise { + this.sessionParams = { ...this.sessionParams, ...params } as SessionParams; + if (params.username) { + this._lastUsername = params.username; } - - private cloneCodeKey(host: KeeperHost, username: string): string { - return `${host}::${username}` + } + + public setLastUsername(username: string): void { + this._lastUsername = username; + } + + private async loadKeeperConfig(): Promise { + if (this._keeperConfig) return this._keeperConfig; + this._keeperConfig = await this.configLoader.load(); + return this._keeperConfig; + } + + private async findDeviceInKeeperConfig( + username: string, + ): Promise> { + const normalizedUsername = username.toLowerCase(); + if (this._deviceCache?.username === normalizedUsername) { + return this._deviceCache.device; } - public async getCloneCode(host: KeeperHost, username: string): Promise> { - const hostStr = String(host) - - const key = this.cloneCodeKey(host, username) - const sessionCode = this.sessionCloneCodes.get(key) - if (sessionCode) return sessionCode - - const device = await this.findDeviceInKeeperConfig(username) - if (device) { - const serverInfo = device.serverInfo.find((entry) => entry.server === hostStr) - if (serverInfo) { - return normal64Bytes(serverInfo.clone_code) - } + const device = await this.lookupDeviceInKeeperConfig(normalizedUsername); + this._deviceCache = { username: normalizedUsername, device }; + return device; + } + + private async lookupDeviceInKeeperConfig( + normalizedUsername: string, + ): Promise> { + const keeperConfig = await this.loadKeeperConfig(); + + if (keeperConfig.users && keeperConfig.devices) { + const user = keeperConfig.users.find( + (configUser) => configUser.user?.toLowerCase() === normalizedUsername, + ); + if (user?.last_device?.device_token) { + const deviceTokenStr = user.last_device.device_token; + const device = keeperConfig.devices.find( + (configDevice) => configDevice.device_token === deviceTokenStr, + ); + if (device?.private_key) { + return { + deviceToken: normal64Bytes(deviceTokenStr), + privateKey: normal64Bytes(device.private_key), + serverInfo: (device.server_info || []).filter( + (entry): entry is Required => + !!entry.server && !!entry.clone_code, + ), + }; } - - return null + } } - public async saveCloneCode(host: KeeperHost, username: string, cloneCode: Uint8Array): Promise { - const key = this.cloneCodeKey(host, username) - this.sessionCloneCodes.set(key, cloneCode) - await this.updateKeeperConfigCloneCode(String(host), username, cloneCode) + if ( + keeperConfig.device_token && + keeperConfig.private_key && + keeperConfig.user?.toLowerCase() === normalizedUsername + ) { + const serverInfo: Array> = []; + const server = keeperConfig.last_server || keeperConfig.server; + if (server && keeperConfig.clone_code) { + serverInfo.push({ server, clone_code: keeperConfig.clone_code }); + } + return { + deviceToken: normal64Bytes(keeperConfig.device_token), + privateKey: normal64Bytes(keeperConfig.private_key), + serverInfo, + }; } - private async updateKeeperConfigCloneCode(host: string, username: string, cloneCode: Uint8Array): Promise { - try { - const parsed = await this.configLoader.load() - if (!parsed || Object.keys(parsed).length === 0) return - - let updated = false - const encodedCloneCode = Buffer.from(cloneCode).toString('base64url') - - const server = parsed.last_server || parsed.server - if (parsed.user?.toLowerCase() === username.toLowerCase() && server === host) { - parsed.clone_code = encodedCloneCode - updated = true - } - - const user = (parsed.users || []).find( - (configUser) => configUser.user?.toLowerCase() === username.toLowerCase() - ) - if (user?.last_device?.device_token) { - const device = (parsed.devices || []).find( - (configDevice) => configDevice.device_token === user.last_device!.device_token - ) - if (device?.server_info) { - const serverInfo = device.server_info.find((entry) => entry.server === host) - if (serverInfo) { - serverInfo.clone_code = encodedCloneCode - updated = true - } - } - } - - if (updated) { - await this.configLoader.save(parsed) - this._keeperConfig = null - this._deviceCache = null - } - } catch (err) { - logger.warn('Failed to update keeper config clone code:', extractErrorMessage(err)) - } - } - - public async getSessionParameters(): Promise> { - return this.sessionParams - } + return null; + } - public async saveSessionParameters(params: Partial): Promise { - this.sessionParams = { ...this.sessionParams, ...params } as SessionParams - if (params.username) { - this._lastUsername = params.username - } - } - - public setLastUsername(username: string): void { - this._lastUsername = username - } - - private async loadKeeperConfig(): Promise { - if (this._keeperConfig) return this._keeperConfig - this._keeperConfig = await this.configLoader.load() - return this._keeperConfig - } - - private async findDeviceInKeeperConfig(username: string): Promise> { - const normalizedUsername = username.toLowerCase() - if (this._deviceCache?.username === normalizedUsername) { - return this._deviceCache.device - } - - const device = await this.lookupDeviceInKeeperConfig(normalizedUsername) - this._deviceCache = { username: normalizedUsername, device } - return device - } - - private async lookupDeviceInKeeperConfig(normalizedUsername: string): Promise> { - const keeperConfig = await this.loadKeeperConfig() - - if (keeperConfig.users && keeperConfig.devices) { - const user = keeperConfig.users.find( - (configUser) => configUser.user?.toLowerCase() === normalizedUsername - ) - if (user?.last_device?.device_token) { - const deviceTokenStr = user.last_device.device_token - const device = keeperConfig.devices.find((configDevice) => configDevice.device_token === deviceTokenStr) - if (device?.private_key) { - return { - deviceToken: normal64Bytes(deviceTokenStr), - privateKey: normal64Bytes(device.private_key), - serverInfo: (device.server_info || []).filter( - (entry): entry is Required => - !!entry.server && !!entry.clone_code - ), - } - } - } - } - - if ( - keeperConfig.device_token && - keeperConfig.private_key && - keeperConfig.user?.toLowerCase() === normalizedUsername - ) { - const serverInfo: Array> = [] - const server = keeperConfig.last_server || keeperConfig.server - if (server && keeperConfig.clone_code) { - serverInfo.push({ server, clone_code: keeperConfig.clone_code }) - } - return { - deviceToken: normal64Bytes(keeperConfig.device_token), - privateKey: normal64Bytes(keeperConfig.private_key), - serverInfo, - } - } - - return null - } - - public static isValidKeeperConfig(value: unknown): value is KeeperJsonConfig { - return isValidKeeperConfig(value) - } + public static isValidKeeperConfig(value: unknown): value is KeeperJsonConfig { + return isValidKeeperConfig(value); + } } diff --git a/KeeperSdk/src/auth/UnavailableAuthUI.ts b/KeeperSdk/src/auth/UnavailableAuthUI.ts index 16278777..c706b99a 100644 --- a/KeeperSdk/src/auth/UnavailableAuthUI.ts +++ b/KeeperSdk/src/auth/UnavailableAuthUI.ts @@ -1,23 +1,30 @@ -import type { AuthUI3, DeviceApprovalChannel, TwoFactorChannelData } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' +import type { + AuthUI3, + DeviceApprovalChannel, + TwoFactorChannelData, +} from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; const MSG = - 'Console authentication is disabled (useConsoleAuth: false). Provide authUI or use a host that handles login (e.g. keeper-shell password prompt).' + "Console authentication is disabled (useConsoleAuth: false). Provide authUI or use a host that handles login (e.g. keeper-shell password prompt)."; /** Placeholder when Node readline-based ConsoleAuthUI must not be used. */ export class UnavailableAuthUI implements AuthUI3 { - public async waitForDeviceApproval(_channels: DeviceApprovalChannel[], _isCloud: boolean): Promise { - throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN) - } + public async waitForDeviceApproval( + _channels: DeviceApprovalChannel[], + _isCloud: boolean, + ): Promise { + throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN); + } - public async waitForTwoFactorCode( - _channels: TwoFactorChannelData[], - _cancel: Promise - ): Promise { - throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN) - } + public async waitForTwoFactorCode( + _channels: TwoFactorChannelData[], + _cancel: Promise, + ): Promise { + throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN); + } - public async getPassword(_isAlternate: boolean): Promise { - throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN) - } + public async getPassword(_isAlternate: boolean): Promise { + throw new KeeperSdkError(MSG, ResultCodes.NOT_LOGGED_IN); + } } diff --git a/KeeperSdk/src/auth/config.ts b/KeeperSdk/src/auth/config.ts index af3d2dd7..1f0b7641 100644 --- a/KeeperSdk/src/auth/config.ts +++ b/KeeperSdk/src/auth/config.ts @@ -1,42 +1,42 @@ export type ConfigurationUser = { - user?: string - server?: string - last_device?: { device_token?: string } -} + user?: string; + server?: string; + last_device?: { device_token?: string }; +}; export type ConfigurationServerConfig = { - server?: string - clone_code?: string -} + server?: string; + clone_code?: string; +}; export type ConfigurationDeviceConfig = { - device_token?: string - private_key?: string - server_info?: Array -} + device_token?: string; + private_key?: string; + server_info?: Array; +}; export type KeeperJsonConfig = { - last_login?: string - last_server?: string - user?: string - server?: string - device_token?: string - private_key?: string - clone_code?: string - users?: Array - devices?: Array -} + last_login?: string; + last_server?: string; + user?: string; + server?: string; + device_token?: string; + private_key?: string; + clone_code?: string; + users?: Array; + devices?: Array; +}; export interface ConfigLoader { - load(): Promise - save(config: KeeperJsonConfig): Promise - readonly configDir: string + load(): Promise; + save(config: KeeperJsonConfig): Promise; + readonly configDir: string; } export function isValidKeeperConfig(value: unknown): value is KeeperJsonConfig { - if (typeof value !== 'object' || value === null) return false - const obj = value as Record - if (obj.users !== undefined && !Array.isArray(obj.users)) return false - if (obj.devices !== undefined && !Array.isArray(obj.devices)) return false - return true + if (typeof value !== "object" || value === null) return false; + const obj = value as Record; + if (obj.users !== undefined && !Array.isArray(obj.users)) return false; + if (obj.devices !== undefined && !Array.isArray(obj.devices)) return false; + return true; } diff --git a/KeeperSdk/src/auth/node/FileConfigLoader.ts b/KeeperSdk/src/auth/node/FileConfigLoader.ts index 61c4d08e..f70a96f1 100644 --- a/KeeperSdk/src/auth/node/FileConfigLoader.ts +++ b/KeeperSdk/src/auth/node/FileConfigLoader.ts @@ -1,36 +1,37 @@ -import fs from 'fs/promises' -import path from 'path' -import os from 'os' -import type { ConfigLoader, KeeperJsonConfig } from '../config' -import { isValidKeeperConfig } from '../config' -import { logger, extractErrorMessage, SdkDefaults } from '../../utils' +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import type { ConfigLoader, KeeperJsonConfig } from "../config"; +import { isValidKeeperConfig } from "../config"; +import { logger, extractErrorMessage, SdkDefaults } from "../../utils"; /** Node-only: read/write `~/.keeper/config.json`. */ export class FileConfigLoader implements ConfigLoader { - public readonly configDir: string + public readonly configDir: string; - constructor(configDir?: string) { - this.configDir = configDir || path.join(os.homedir(), SdkDefaults.CONFIG_DIR) - } + constructor(configDir?: string) { + this.configDir = + configDir || path.join(os.homedir(), SdkDefaults.CONFIG_DIR); + } - async load(): Promise { - const configPath = path.join(this.configDir, 'config.json') - try { - const content = await fs.readFile(configPath, 'utf-8') - const parsed: unknown = JSON.parse(content) - if (isValidKeeperConfig(parsed)) { - return parsed - } - } catch (err) { - logger.debug('Failed to load keeper config:', extractErrorMessage(err)) - } - return {} + async load(): Promise { + const configPath = path.join(this.configDir, "config.json"); + try { + const content = await fs.readFile(configPath, "utf-8"); + const parsed: unknown = JSON.parse(content); + if (isValidKeeperConfig(parsed)) { + return parsed; + } + } catch (err) { + logger.debug("Failed to load keeper config:", extractErrorMessage(err)); } + return {}; + } - async save(config: KeeperJsonConfig): Promise { - const configPath = path.join(this.configDir, 'config.json') - await fs.writeFile(configPath, JSON.stringify(config, null, 2), { - mode: 0o600, - }) - } + async save(config: KeeperJsonConfig): Promise { + const configPath = path.join(this.configDir, "config.json"); + await fs.writeFile(configPath, JSON.stringify(config, null, 2), { + mode: 0o600, + }); + } } diff --git a/KeeperSdk/src/auth/sessionRestore.ts b/KeeperSdk/src/auth/sessionRestore.ts index 0e64a4e0..beff79bb 100644 --- a/KeeperSdk/src/auth/sessionRestore.ts +++ b/KeeperSdk/src/auth/sessionRestore.ts @@ -1,153 +1,198 @@ -import { Authentication, normal64Bytes, type SessionParams } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' +import { + Authentication, + normal64Bytes, + type SessionParams, +} from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; -type UserType = SessionParams['userType'] +type UserType = SessionParams["userType"]; const UserTypeValues = { - normal: 'normal' as UserType, - cloudSso: 'cloud_sso' as UserType, - onsiteSso: 'onsite_sso' as UserType, -} + normal: "normal" as UserType, + cloudSso: "cloud_sso" as UserType, + onsiteSso: "onsite_sso" as UserType, +}; /** String form of {@link SessionParams} as exported from extension / vault storage. */ export type SessionRestoreInput = { - accountUid: string - clientKey: string - dataKey: string - eccPrivateKey: string - eccPublicKey: string - messageSessionUid: string - privateKey: string - sessionToken: string - sessionTokenType: number | string - username: string - userType: number | string - ssoLogoutUrl?: string - ssoSessionId?: string - enterprisePublicKey?: string - enterpriseEccPublicKey?: string -} + accountUid: string; + clientKey: string; + dataKey: string; + eccPrivateKey: string; + eccPublicKey: string; + messageSessionUid: string; + privateKey: string; + sessionToken: string; + sessionTokenType: number | string; + username: string; + userType: number | string; + ssoLogoutUrl?: string; + ssoSessionId?: string; + enterprisePublicKey?: string; + enterpriseEccPublicKey?: string; +}; const REQUIRED_KEYS: (keyof SessionRestoreInput)[] = [ - 'accountUid', - 'clientKey', - 'dataKey', - 'eccPrivateKey', - 'eccPublicKey', - 'messageSessionUid', - 'privateKey', - 'sessionToken', - 'sessionTokenType', - 'username', - 'userType', -] + "accountUid", + "clientKey", + "dataKey", + "eccPrivateKey", + "eccPublicKey", + "messageSessionUid", + "privateKey", + "sessionToken", + "sessionTokenType", + "username", + "userType", +]; -function decodeBytes(label: string, value: string | undefined, required: boolean): Uint8Array | undefined { - const trimmed = typeof value === 'string' ? value.trim() : '' - if (!trimmed) { - if (required) { - throw new KeeperSdkError(`restore-session: missing required field: ${label}`, ResultCodes.MISSING_USERNAME) - } - return undefined - } - try { - return normal64Bytes(trimmed) - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - throw new KeeperSdkError(`restore-session: invalid base64 for ${label}: ${msg}`, ResultCodes.INVALID_CREDENTIALS) +function decodeBytes( + label: string, + value: string | undefined, + required: boolean, +): Uint8Array | undefined { + const trimmed = typeof value === "string" ? value.trim() : ""; + if (!trimmed) { + if (required) { + throw new KeeperSdkError( + `restore-session: missing required field: ${label}`, + ResultCodes.MISSING_USERNAME, + ); } + return undefined; + } + try { + return normal64Bytes(trimmed); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + throw new KeeperSdkError( + `restore-session: invalid base64 for ${label}: ${msg}`, + ResultCodes.INVALID_CREDENTIALS, + ); + } } function parseUserType(value: number | string): UserType { - if (typeof value === 'number') { - switch (value) { - case 0: - return UserTypeValues.normal - case 1: - return UserTypeValues.cloudSso - case 2: - return UserTypeValues.onsiteSso - default: - break - } + if (typeof value === "number") { + switch (value) { + case 0: + return UserTypeValues.normal; + case 1: + return UserTypeValues.cloudSso; + case 2: + return UserTypeValues.onsiteSso; + default: + break; } - const s = String(value).toLowerCase() - if (s === 'normal' || s === '0') return UserTypeValues.normal - if (s === 'cloud_sso' || s === 'cloudsso' || s === '1') return UserTypeValues.cloudSso - if (s === 'onsite_sso' || s === 'onsitesso' || s === '2') return UserTypeValues.onsiteSso - throw new KeeperSdkError(`restore-session: unknown userType: ${value}`, ResultCodes.INVALID_CREDENTIALS) + } + const s = String(value).toLowerCase(); + if (s === "normal" || s === "0") return UserTypeValues.normal; + if (s === "cloud_sso" || s === "cloudsso" || s === "1") + return UserTypeValues.cloudSso; + if (s === "onsite_sso" || s === "onsitesso" || s === "2") + return UserTypeValues.onsiteSso; + throw new KeeperSdkError( + `restore-session: unknown userType: ${value}`, + ResultCodes.INVALID_CREDENTIALS, + ); } -function parseSessionTokenType(value: number | string): Authentication.SessionTokenType { - const n = typeof value === 'number' ? value : Number(value) - if (!Number.isFinite(n)) { - throw new KeeperSdkError(`restore-session: invalid sessionTokenType: ${value}`, ResultCodes.INVALID_CREDENTIALS) - } - return n as Authentication.SessionTokenType +function parseSessionTokenType( + value: number | string, +): Authentication.SessionTokenType { + const n = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(n)) { + throw new KeeperSdkError( + `restore-session: invalid sessionTokenType: ${value}`, + ResultCodes.INVALID_CREDENTIALS, + ); + } + return n as Authentication.SessionTokenType; } -export function validateSessionRestoreInput(input: Partial): SessionRestoreInput { - const missing = REQUIRED_KEYS.filter((k) => { - const v = input[k] - return v === undefined || v === null || (typeof v === 'string' && !v.trim()) - }) - if (missing.length > 0) { - throw new KeeperSdkError( - `restore-session: missing required field(s): ${missing.join(', ')}`, - ResultCodes.MISSING_USERNAME - ) - } - return input as SessionRestoreInput +export function validateSessionRestoreInput( + input: Partial, +): SessionRestoreInput { + const missing = REQUIRED_KEYS.filter((k) => { + const v = input[k]; + return ( + v === undefined || v === null || (typeof v === "string" && !v.trim()) + ); + }); + if (missing.length > 0) { + throw new KeeperSdkError( + `restore-session: missing required field(s): ${missing.join(", ")}`, + ResultCodes.MISSING_USERNAME, + ); + } + return input as SessionRestoreInput; } /** Build keeperapi {@link SessionParams} from extension-style strings. */ export function toSessionParams(input: SessionRestoreInput): SessionParams { - const enterprisePublicKey = decodeBytes('enterprisePublicKey', input.enterprisePublicKey, false) - const enterpriseEccPublicKey = decodeBytes('enterpriseEccPublicKey', input.enterpriseEccPublicKey, false) + const enterprisePublicKey = decodeBytes( + "enterprisePublicKey", + input.enterprisePublicKey, + false, + ); + const enterpriseEccPublicKey = decodeBytes( + "enterpriseEccPublicKey", + input.enterpriseEccPublicKey, + false, + ); - return { - accountUid: decodeBytes('accountUid', input.accountUid, true)!, - clientKey: decodeBytes('clientKey', input.clientKey, true)!, - dataKey: decodeBytes('dataKey', input.dataKey, true)!, - eccPrivateKey: decodeBytes('eccPrivateKey', input.eccPrivateKey, true)!, - eccPublicKey: decodeBytes('eccPublicKey', input.eccPublicKey, true)!, - messageSessionUid: decodeBytes('messageSessionUid', input.messageSessionUid, true)!, - privateKey: decodeBytes('privateKey', input.privateKey, true)!, - sessionToken: input.sessionToken.trim(), - sessionTokenType: parseSessionTokenType(input.sessionTokenType), - username: input.username.trim(), - userType: parseUserType(input.userType), - ssoLogoutUrl: input.ssoLogoutUrl?.trim() ?? '', - ssoSessionId: input.ssoSessionId?.trim() ?? '', - ...(enterprisePublicKey ? { enterprisePublicKey } : {}), - ...(enterpriseEccPublicKey ? { enterpriseEccPublicKey } : {}), - } + return { + accountUid: decodeBytes("accountUid", input.accountUid, true)!, + clientKey: decodeBytes("clientKey", input.clientKey, true)!, + dataKey: decodeBytes("dataKey", input.dataKey, true)!, + eccPrivateKey: decodeBytes("eccPrivateKey", input.eccPrivateKey, true)!, + eccPublicKey: decodeBytes("eccPublicKey", input.eccPublicKey, true)!, + messageSessionUid: decodeBytes( + "messageSessionUid", + input.messageSessionUid, + true, + )!, + privateKey: decodeBytes("privateKey", input.privateKey, true)!, + sessionToken: input.sessionToken.trim(), + sessionTokenType: parseSessionTokenType(input.sessionTokenType), + username: input.username.trim(), + userType: parseUserType(input.userType), + ssoLogoutUrl: input.ssoLogoutUrl?.trim() ?? "", + ssoSessionId: input.ssoSessionId?.trim() ?? "", + ...(enterprisePublicKey ? { enterprisePublicKey } : {}), + ...(enterpriseEccPublicKey ? { enterpriseEccPublicKey } : {}), + }; } function assertBodyIsJsonNotHtml(body: string, source: string): void { - const head = body.trimStart().slice(0, 32).toLowerCase() - if (head.startsWith(') + } + if (typeof parsed !== "object" || parsed === null) { + throw new KeeperSdkError( + "restore-session: JSON must be an object", + ResultCodes.INVALID_CREDENTIALS, + ); + } + return validateSessionRestoreInput(parsed as Partial); } /** Parse `--from-json` payload, or read a file when the value is not JSON. */ export async function resolveSessionRestorePayload( - raw: string, - readFile?: (path: string) => Promise + raw: string, + readFile?: (path: string) => Promise, ): Promise { - const text = raw.trim() - if (readFile && looksLikeFilePath(text)) { - const body = await readFile(text) - assertBodyIsJsonNotHtml(body, text) - return sessionRestoreFromJson(body) - } - try { - return sessionRestoreFromJson(text) - } catch (e) { - if (looksLikeInlineJson(text) || !readFile) { - throw e - } - const body = await readFile(text) - assertBodyIsJsonNotHtml(body, text) - return sessionRestoreFromJson(body) + const text = raw.trim(); + if (readFile && looksLikeFilePath(text)) { + const body = await readFile(text); + assertBodyIsJsonNotHtml(body, text); + return sessionRestoreFromJson(body); + } + try { + return sessionRestoreFromJson(text); + } catch (e) { + if (looksLikeInlineJson(text) || !readFile) { + throw e; } + const body = await readFile(text); + assertBodyIsJsonNotHtml(body, text); + return sessionRestoreFromJson(body); + } } diff --git a/KeeperSdk/src/browser.ts b/KeeperSdk/src/browser.ts index 463daba6..87898979 100644 --- a/KeeperSdk/src/browser.ts +++ b/KeeperSdk/src/browser.ts @@ -1,6 +1,6 @@ -import { connectSdkPlatform } from './platform' -import { browserSdkPlatform } from './platform/browser/platform' +import { connectSdkPlatform } from "./platform"; +import { browserSdkPlatform } from "./platform/browser/platform"; -connectSdkPlatform(browserSdkPlatform) +connectSdkPlatform(browserSdkPlatform); -export * from './api' +export * from "./api"; diff --git a/KeeperSdk/src/folders/FolderManager.ts b/KeeperSdk/src/folders/FolderManager.ts index 54e07cbe..fb73f84d 100644 --- a/KeeperSdk/src/folders/FolderManager.ts +++ b/KeeperSdk/src/folders/FolderManager.ts @@ -1,174 +1,264 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError, ResultCodes } from '../utils' -import { addFolder, mkdir } from './addFolder' -import type { AddFolderInput, AddFolderResult, MkdirOptions } from './addFolder' +import type { Auth } from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { KeeperSdkError, ResultCodes } from "../utils"; +import { addFolder, mkdir } from "./addFolder"; +import type { + AddFolderInput, + AddFolderResult, + MkdirOptions, +} from "./addFolder"; import { - changeDirectory, - createVaultFolderSession, - findParentFolderUid, - getWorkingFolderDisplayName, - resolveSingleFolder, - splitPathComponents, - tryResolvePath, -} from './changeDirectory' -import type { ChangeDirectoryResult, TryResolvePathResult, VaultFolderSession } from './changeDirectory' -import { buildFolderDeleteObject, deleteFolder, resolveRmdirPatternsToFolderUids, rmdir } from './deleteFolder' -import type { DeleteFolderResult, RmdirOptions } from './deleteFolder' -import { buildFolderTree, folderTreeAscii, renderFolderTreeAscii } from './folderTree' -import type { FolderTreeBuildOptions, FolderTreeResult } from './folderTree' -import { findFolder, getFolder } from './getFolder' -import type { FoundFolder, GetFolderOptions, GetFolderResult } from './getFolder' -import { findFolderUidByNameOrUid, listFolder, listRootUserFolders, listVaultRootFolders } from './listFolder' -import type { ListFolderFolderSimple, ListFolderOptions, ListFolderResult } from './listFolder' -import { renameFolder, updateFolder, updateSharedFolderPermissions } from './updateFolder' -import type { RenameFolderResult, UpdateFolderInput, UpdateFolderResult } from './updateFolder' - -export type AuthProvider = () => Auth + changeDirectory, + createVaultFolderSession, + findParentFolderUid, + getWorkingFolderDisplayName, + resolveSingleFolder, + splitPathComponents, + tryResolvePath, +} from "./changeDirectory"; +import type { + ChangeDirectoryResult, + TryResolvePathResult, + VaultFolderSession, +} from "./changeDirectory"; +import { + buildFolderDeleteObject, + deleteFolder, + resolveRmdirPatternsToFolderUids, + rmdir, +} from "./deleteFolder"; +import type { DeleteFolderResult, RmdirOptions } from "./deleteFolder"; +import { + buildFolderTree, + folderTreeAscii, + renderFolderTreeAscii, +} from "./folderTree"; +import type { FolderTreeBuildOptions, FolderTreeResult } from "./folderTree"; +import { findFolder, getFolder } from "./getFolder"; +import type { + FoundFolder, + GetFolderOptions, + GetFolderResult, +} from "./getFolder"; +import { + findFolderUidByNameOrUid, + listFolder, + listRootUserFolders, + listVaultRootFolders, +} from "./listFolder"; +import type { + ListFolderFolderSimple, + ListFolderOptions, + ListFolderResult, +} from "./listFolder"; +import { + renameFolder, + updateFolder, + updateSharedFolderPermissions, +} from "./updateFolder"; +import type { + RenameFolderResult, + UpdateFolderInput, + UpdateFolderResult, +} from "./updateFolder"; + +export type AuthProvider = () => Auth; export type SharedFolderPermissionsInput = { - manageUsers?: boolean | null - manageRecords?: boolean | null - canShare?: boolean | null - canEdit?: boolean | null -} + manageUsers?: boolean | null; + manageRecords?: boolean | null; + canShare?: boolean | null; + canEdit?: boolean | null; +}; export class FolderManager { - private readonly storage: InMemoryStorage - private readonly session: VaultFolderSession - private readonly authProvider: AuthProvider - - constructor(storage: InMemoryStorage, session: VaultFolderSession, authProvider: AuthProvider) { - this.storage = storage - this.session = session - this.authProvider = authProvider - } - - public static createSession(): VaultFolderSession { - return createVaultFolderSession() - } - - public static splitPathComponents(path: string): string[] { - return splitPathComponents(path) - } - - public getSession(): VaultFolderSession { - return this.session - } - - public getCurrentFolderUid(): string | null { - return this.session.currentFolderUid - } - - public getWorkingFolderDisplayName(): string { - return getWorkingFolderDisplayName(this.storage, this.session.currentFolderUid) - } - - private requireAuth(): Auth { - const auth = this.authProvider() - if (!auth) { - throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN) - } - return auth - } - - public async listFolder(options: ListFolderOptions = {}): Promise { - return listFolder(this.storage, options) - } - - public listRootUserFolders() { - return listRootUserFolders(this.storage) - } - - public async listVaultRootFolders(): Promise<{ - rows: ListFolderFolderSimple[] - promotedRootSharedUids: Set - }> { - return listVaultRootFolders(this.storage) - } - - public findFolderUidByNameOrUid(nameOrUid: string): string | undefined { - return findFolderUidByNameOrUid(this.storage, nameOrUid) - } - - public findFolder(nameOrUid: string): FoundFolder | undefined { - return findFolder(this.storage, nameOrUid) - } - - public async findParentFolderUid(folderUid: string): Promise { - return findParentFolderUid(this.storage, folderUid) - } - - public async getFolder(uidOrName: string, options: GetFolderOptions = {}): Promise { - return getFolder(this.storage, uidOrName, options) - } - - public async changeDirectory(path: string): Promise { - return changeDirectory(this.storage, this.session, path) - } - - public async tryResolvePath(path: string): Promise { - return tryResolvePath(this.storage, this.session, path) - } - - public async resolveSingleFolder(folderName: string): Promise { - return resolveSingleFolder(this.storage, this.session, folderName) - } - - public async addFolder(input: AddFolderInput): Promise { - return addFolder(this.requireAuth(), this.storage, input) - } - - public async mkdir( - path: string, - options: MkdirOptions = {} - ): Promise<{ folderUid: string; success: boolean; message?: string }> { - return mkdir(this.requireAuth(), this.storage, this.session, path, options) - } - - public async updateFolder(input: UpdateFolderInput): Promise { - return updateFolder(this.requireAuth(), this.storage, input) - } - - public async renameFolder(folderPath: string, newName: string): Promise { - return renameFolder(this.requireAuth(), this.storage, this.session, folderPath, newName) - } - - public async updateSharedFolderPermissions( - sharedFolderUid: string, - permissions: SharedFolderPermissionsInput - ): Promise { - return updateSharedFolderPermissions(this.requireAuth(), this.storage, sharedFolderUid, permissions) - } - - public async deleteFolder( - folderRefs: string[], - confirm?: (summary: string) => boolean | Promise - ): Promise { - return deleteFolder(this.requireAuth(), this.storage, folderRefs, confirm) - } - - public async rmdir(patterns: string[], options: RmdirOptions = {}): Promise { - return rmdir(this.requireAuth(), this.storage, this.session, patterns, options) - } - - public async resolveRmdirPatternsToFolderUids(pattern: string): Promise> { - return resolveRmdirPatternsToFolderUids(this.storage, this.session, pattern) - } - - public async buildFolderDeleteObject(folderUid: string) { - return buildFolderDeleteObject(this.storage, folderUid) - } - - public async buildFolderTree(options: FolderTreeBuildOptions = {}): Promise { - return buildFolderTree(this.storage, this.session, options) - } - - public async folderTreeAscii(options: FolderTreeBuildOptions = {}): Promise { - return folderTreeAscii(this.storage, this.session, options) - } - - public renderFolderTreeAscii(tree: FolderTreeResult): string { - return renderFolderTreeAscii(tree) + private readonly storage: InMemoryStorage; + private readonly session: VaultFolderSession; + private readonly authProvider: AuthProvider; + + constructor( + storage: InMemoryStorage, + session: VaultFolderSession, + authProvider: AuthProvider, + ) { + this.storage = storage; + this.session = session; + this.authProvider = authProvider; + } + + public static createSession(): VaultFolderSession { + return createVaultFolderSession(); + } + + public static splitPathComponents(path: string): string[] { + return splitPathComponents(path); + } + + public getSession(): VaultFolderSession { + return this.session; + } + + public getCurrentFolderUid(): string | null { + return this.session.currentFolderUid; + } + + public getWorkingFolderDisplayName(): string { + return getWorkingFolderDisplayName( + this.storage, + this.session.currentFolderUid, + ); + } + + private requireAuth(): Auth { + const auth = this.authProvider(); + if (!auth) { + throw new KeeperSdkError( + "Not logged in. Call login() first.", + ResultCodes.NOT_LOGGED_IN, + ); } + return auth; + } + + public async listFolder( + options: ListFolderOptions = {}, + ): Promise { + return listFolder(this.storage, options); + } + + public listRootUserFolders() { + return listRootUserFolders(this.storage); + } + + public async listVaultRootFolders(): Promise<{ + rows: ListFolderFolderSimple[]; + promotedRootSharedUids: Set; + }> { + return listVaultRootFolders(this.storage); + } + + public findFolderUidByNameOrUid(nameOrUid: string): string | undefined { + return findFolderUidByNameOrUid(this.storage, nameOrUid); + } + + public findFolder(nameOrUid: string): FoundFolder | undefined { + return findFolder(this.storage, nameOrUid); + } + + public async findParentFolderUid(folderUid: string): Promise { + return findParentFolderUid(this.storage, folderUid); + } + + public async getFolder( + uidOrName: string, + options: GetFolderOptions = {}, + ): Promise { + return getFolder(this.storage, uidOrName, options); + } + + public async changeDirectory(path: string): Promise { + return changeDirectory(this.storage, this.session, path); + } + + public async tryResolvePath(path: string): Promise { + return tryResolvePath(this.storage, this.session, path); + } + + public async resolveSingleFolder( + folderName: string, + ): Promise { + return resolveSingleFolder(this.storage, this.session, folderName); + } + + public async addFolder(input: AddFolderInput): Promise { + return addFolder(this.requireAuth(), this.storage, input); + } + + public async mkdir( + path: string, + options: MkdirOptions = {}, + ): Promise<{ folderUid: string; success: boolean; message?: string }> { + return mkdir(this.requireAuth(), this.storage, this.session, path, options); + } + + public async updateFolder( + input: UpdateFolderInput, + ): Promise { + return updateFolder(this.requireAuth(), this.storage, input); + } + + public async renameFolder( + folderPath: string, + newName: string, + ): Promise { + return renameFolder( + this.requireAuth(), + this.storage, + this.session, + folderPath, + newName, + ); + } + + public async updateSharedFolderPermissions( + sharedFolderUid: string, + permissions: SharedFolderPermissionsInput, + ): Promise { + return updateSharedFolderPermissions( + this.requireAuth(), + this.storage, + sharedFolderUid, + permissions, + ); + } + + public async deleteFolder( + folderRefs: string[], + confirm?: (summary: string) => boolean | Promise, + ): Promise { + return deleteFolder(this.requireAuth(), this.storage, folderRefs, confirm); + } + + public async rmdir( + patterns: string[], + options: RmdirOptions = {}, + ): Promise { + return rmdir( + this.requireAuth(), + this.storage, + this.session, + patterns, + options, + ); + } + + public async resolveRmdirPatternsToFolderUids( + pattern: string, + ): Promise> { + return resolveRmdirPatternsToFolderUids( + this.storage, + this.session, + pattern, + ); + } + + public async buildFolderDeleteObject(folderUid: string) { + return buildFolderDeleteObject(this.storage, folderUid); + } + + public async buildFolderTree( + options: FolderTreeBuildOptions = {}, + ): Promise { + return buildFolderTree(this.storage, this.session, options); + } + + public async folderTreeAscii( + options: FolderTreeBuildOptions = {}, + ): Promise { + return folderTreeAscii(this.storage, this.session, options); + } + + public renderFolderTreeAscii(tree: FolderTreeResult): string { + return renderFolderTreeAscii(tree); + } } diff --git a/KeeperSdk/src/folders/addFolder.ts b/KeeperSdk/src/folders/addFolder.ts index c90b724c..0c8197bd 100644 --- a/KeeperSdk/src/folders/addFolder.ts +++ b/KeeperSdk/src/folders/addFolder.ts @@ -1,295 +1,391 @@ -import type { Auth, FolderAddRequest } from '@keeper-security/keeperapi' +import type { Auth, FolderAddRequest } from "@keeper-security/keeperapi"; import { - encryptForStorage, - encryptObjectForStorage, - folderAddCommand, - generateEncryptionKey, - generateUid, - platform, -} from '@keeper-security/keeperapi' -import type { DSharedFolder, DSharedFolderFolder, DUserFolder } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { isBoolean, KeeperSdkError, extractErrorMessage } from '../utils' -import { listFolder } from './listFolder' -import { tryResolvePath, splitPathComponents, type VaultFolderSession } from './changeDirectory' -import { FolderKind, FolderResultStatus, ParentFolderKind, validateFolderName } from './folderHelpers' - -type NewFolderKind = FolderKind + encryptForStorage, + encryptObjectForStorage, + folderAddCommand, + generateEncryptionKey, + generateUid, + platform, +} from "@keeper-security/keeperapi"; +import type { + DSharedFolder, + DSharedFolderFolder, + DUserFolder, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { isBoolean, KeeperSdkError, extractErrorMessage } from "../utils"; +import { listFolder } from "./listFolder"; +import { + tryResolvePath, + splitPathComponents, + type VaultFolderSession, +} from "./changeDirectory"; +import { + FolderKind, + FolderResultStatus, + ParentFolderKind, + validateFolderName, +} from "./folderHelpers"; + +type NewFolderKind = FolderKind; export type AddFolderInput = { - folderName: string - isSharedFolder?: boolean - parentUid?: string | null - manageUsers?: boolean - manageRecords?: boolean - canShare?: boolean - canEdit?: boolean -} + folderName: string; + isSharedFolder?: boolean; + parentUid?: string | null; + manageUsers?: boolean; + manageRecords?: boolean; + canShare?: boolean; + canEdit?: boolean; +}; export type AddFolderResult = { - folderUid: string - success: boolean - message?: string -} + folderUid: string; + success: boolean; + message?: string; +}; export type MkdirOptions = { - sharedFolder?: boolean - userFolder?: boolean - grantAll?: boolean - manageUsers?: boolean - manageRecords?: boolean - canShare?: boolean - canEdit?: boolean -} + sharedFolder?: boolean; + userFolder?: boolean; + grantAll?: boolean; + manageUsers?: boolean; + manageRecords?: boolean; + canShare?: boolean; + canEdit?: boolean; +}; type ParentContext = { - kind: ParentFolderKind - sharedScopeUid: string | null + kind: ParentFolderKind; + sharedScopeUid: string | null; +}; + +function resolveParentContext( + storage: InMemoryStorage, + parentUid: string | null, +): ParentContext { + if (parentUid === null || parentUid === "") { + return { kind: ParentFolderKind.VirtualRoot, sharedScopeUid: null }; + } + if (storage.getByUid(FolderKind.UserFolder, parentUid)) { + return { kind: ParentFolderKind.UserFolder, sharedScopeUid: null }; + } + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + parentUid, + ); + if (sharedFolder) { + return { + kind: ParentFolderKind.SharedFolder, + sharedScopeUid: sharedFolder.uid, + }; + } + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + parentUid, + ); + if (sharedFolderFolder) { + return { + kind: ParentFolderKind.SharedFolderFolder, + sharedScopeUid: sharedFolderFolder.sharedFolderUid, + }; + } + throw new KeeperSdkError( + `Parent folder "${parentUid}" not found`, + "folder_not_found", + ); } -function resolveParentContext(storage: InMemoryStorage, parentUid: string | null): ParentContext { - if (parentUid === null || parentUid === '') { - return { kind: ParentFolderKind.VirtualRoot, sharedScopeUid: null } - } - if (storage.getByUid(FolderKind.UserFolder, parentUid)) { - return { kind: ParentFolderKind.UserFolder, sharedScopeUid: null } - } - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, parentUid) - if (sharedFolder) { - return { kind: ParentFolderKind.SharedFolder, sharedScopeUid: sharedFolder.uid } +function decideNewFolderType( + parent: ParentContext, + isSharedFolder: boolean, +): NewFolderKind { + if (isSharedFolder) { + if ( + parent.kind !== ParentFolderKind.UserFolder && + parent.kind !== ParentFolderKind.VirtualRoot + ) { + throw new KeeperSdkError( + "Shared folders cannot be nested inside other shared folders.", + "shared_folder_nested", + ); } - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, parentUid) - if (sharedFolderFolder) { - return { - kind: ParentFolderKind.SharedFolderFolder, - sharedScopeUid: sharedFolderFolder.sharedFolderUid, - } - } - throw new KeeperSdkError(`Parent folder "${parentUid}" not found`, 'folder_not_found') -} - -function decideNewFolderType(parent: ParentContext, isSharedFolder: boolean): NewFolderKind { - if (isSharedFolder) { - if (parent.kind !== ParentFolderKind.UserFolder && parent.kind !== ParentFolderKind.VirtualRoot) { - throw new KeeperSdkError( - 'Shared folders cannot be nested inside other shared folders.', - 'shared_folder_nested' - ) - } - return FolderKind.SharedFolder - } - if (parent.kind === ParentFolderKind.VirtualRoot || parent.kind === ParentFolderKind.UserFolder) { - return FolderKind.UserFolder - } - return FolderKind.SharedFolderFolder + return FolderKind.SharedFolder; + } + if ( + parent.kind === ParentFolderKind.VirtualRoot || + parent.kind === ParentFolderKind.UserFolder + ) { + return FolderKind.UserFolder; + } + return FolderKind.SharedFolderFolder; } async function getEncryptionKeyForNewFolder( - auth: Auth, - storage: InMemoryStorage, - folderType: NewFolderKind, - sharedScopeUid: string | null + auth: Auth, + storage: InMemoryStorage, + folderType: NewFolderKind, + sharedScopeUid: string | null, ): Promise { - if (folderType === FolderKind.SharedFolderFolder) { - if (!sharedScopeUid) { - throw new KeeperSdkError('Shared folder scope could not be resolved.', 'shared_folder_scope_missing') - } - const sharedFolderKey = await storage.getKeyBytes(sharedScopeUid) - if (!sharedFolderKey) { - throw new KeeperSdkError( - 'Shared folder encryption key not available. Sync the vault and try again.', - 'shared_folder_key_missing' - ) - } - return sharedFolderKey + if (folderType === FolderKind.SharedFolderFolder) { + if (!sharedScopeUid) { + throw new KeeperSdkError( + "Shared folder scope could not be resolved.", + "shared_folder_scope_missing", + ); } - if (!auth.dataKey) { - throw new KeeperSdkError('Data key not available. Ensure you are logged in.', 'data_key_missing') + const sharedFolderKey = await storage.getKeyBytes(sharedScopeUid); + if (!sharedFolderKey) { + throw new KeeperSdkError( + "Shared folder encryption key not available. Sync the vault and try again.", + "shared_folder_key_missing", + ); } - return auth.dataKey + return sharedFolderKey; + } + if (!auth.dataKey) { + throw new KeeperSdkError( + "Data key not available. Ensure you are logged in.", + "data_key_missing", + ); + } + return auth.dataKey; } async function findChildFolderUidByName( - storage: InMemoryStorage, - parentUid: string | null, - name: string + storage: InMemoryStorage, + parentUid: string | null, + name: string, ): Promise { - const result = await listFolder(storage, { - folderUid: parentUid === null ? undefined : parentUid, - showFolders: true, - showRecords: false, - }) - const trimmedName = name.trim() - const lowerName = trimmedName.toLowerCase() - for (const folder of result.folders) { - if (folder.uid === trimmedName) return folder.uid - if (folder.name.trim() === trimmedName) return folder.uid - if (folder.name.trim().toLowerCase() === lowerName) return folder.uid - } - return undefined + const result = await listFolder(storage, { + folderUid: parentUid === null ? undefined : parentUid, + showFolders: true, + showRecords: false, + }); + const trimmedName = name.trim(); + const lowerName = trimmedName.toLowerCase(); + for (const folder of result.folders) { + if (folder.uid === trimmedName) return folder.uid; + if (folder.name.trim() === trimmedName) return folder.uid; + if (folder.name.trim().toLowerCase() === lowerName) return folder.uid; + } + return undefined; } -export async function addFolder(auth: Auth, storage: InMemoryStorage, input: AddFolderInput): Promise { - const name = input.folderName?.trim() - if (!name) { - throw new KeeperSdkError('Folder name cannot be empty.', 'folder_name_required') - } - validateFolderName(name) - - const parentUid = input.parentUid === undefined || input.parentUid === '' ? null : input.parentUid - const parent = resolveParentContext(storage, parentUid) - - const isShared = input.isSharedFolder === true - const folderType = decideNewFolderType(parent, isShared) - - const folderUid = generateUid() - const folderKey = generateEncryptionKey() - - const sharedScope = folderType === FolderKind.SharedFolderFolder ? parent.sharedScopeUid : null - - const encryptionKey = await getEncryptionKeyForNewFolder(auth, storage, folderType, sharedScope) - - const request: FolderAddRequest = { - folder_uid: folderUid, - folder_type: folderType, - key: await encryptForStorage(folderKey, encryptionKey), - data: await encryptObjectForStorage({ name, title: name }, folderKey), - link: false, - } - - if (parentUid) { - request.parent_uid = parentUid - } - if (folderType === FolderKind.SharedFolderFolder && sharedScope) { - request.shared_folder_uid = sharedScope - } - - if (folderType === FolderKind.SharedFolder) { - request.name = await encryptForStorage(platform.stringToBytes(name), folderKey) - request.manage_users = isBoolean(input.manageUsers) ? input.manageUsers : false - request.manage_records = isBoolean(input.manageRecords) ? input.manageRecords : false - request.can_edit = isBoolean(input.canEdit) ? input.canEdit : false - request.can_share = isBoolean(input.canShare) ? input.canShare : false - } - - try { - const response = await auth.executeRestCommand(folderAddCommand(request)) - const succeeded = - response.result === FolderResultStatus.Success || response.result_code === FolderResultStatus.Success - if (!succeeded) { - const reason = - response.message || - response.result_code || - `folder_add failed for "${name}" (uid=${folderUid}, type=${folderType}): server returned no message or result_code` - return { - folderUid, - success: false, - message: reason, - } - } - return { folderUid, success: true } - } catch (err) { - return { - folderUid, - success: false, - message: `folder_add failed for "${name}" (uid=${folderUid}, type=${folderType}): ${extractErrorMessage(err)}`, - } +export async function addFolder( + auth: Auth, + storage: InMemoryStorage, + input: AddFolderInput, +): Promise { + const name = input.folderName?.trim(); + if (!name) { + throw new KeeperSdkError( + "Folder name cannot be empty.", + "folder_name_required", + ); + } + validateFolderName(name); + + const parentUid = + input.parentUid === undefined || input.parentUid === "" + ? null + : input.parentUid; + const parent = resolveParentContext(storage, parentUid); + + const isShared = input.isSharedFolder === true; + const folderType = decideNewFolderType(parent, isShared); + + const folderUid = generateUid(); + const folderKey = generateEncryptionKey(); + + const sharedScope = + folderType === FolderKind.SharedFolderFolder ? parent.sharedScopeUid : null; + + const encryptionKey = await getEncryptionKeyForNewFolder( + auth, + storage, + folderType, + sharedScope, + ); + + const request: FolderAddRequest = { + folder_uid: folderUid, + folder_type: folderType, + key: await encryptForStorage(folderKey, encryptionKey), + data: await encryptObjectForStorage({ name, title: name }, folderKey), + link: false, + }; + + if (parentUid) { + request.parent_uid = parentUid; + } + if (folderType === FolderKind.SharedFolderFolder && sharedScope) { + request.shared_folder_uid = sharedScope; + } + + if (folderType === FolderKind.SharedFolder) { + request.name = await encryptForStorage( + platform.stringToBytes(name), + folderKey, + ); + request.manage_users = isBoolean(input.manageUsers) + ? input.manageUsers + : false; + request.manage_records = isBoolean(input.manageRecords) + ? input.manageRecords + : false; + request.can_edit = isBoolean(input.canEdit) ? input.canEdit : false; + request.can_share = isBoolean(input.canShare) ? input.canShare : false; + } + + try { + const response = await auth.executeRestCommand(folderAddCommand(request)); + const succeeded = + response.result === FolderResultStatus.Success || + response.result_code === FolderResultStatus.Success; + if (!succeeded) { + const reason = + response.message || + response.result_code || + `folder_add failed for "${name}" (uid=${folderUid}, type=${folderType}): server returned no message or result_code`; + return { + folderUid, + success: false, + message: reason, + }; } + return { folderUid, success: true }; + } catch (err) { + return { + folderUid, + success: false, + message: `folder_add failed for "${name}" (uid=${folderUid}, type=${folderType}): ${extractErrorMessage(err)}`, + }; + } } export async function mkdir( - auth: Auth, - storage: InMemoryStorage, - session: VaultFolderSession, - path: string, - options: MkdirOptions = {} + auth: Auth, + storage: InMemoryStorage, + session: VaultFolderSession, + path: string, + options: MkdirOptions = {}, ): Promise<{ folderUid: string; success: boolean; message?: string }> { - const trimmed = path.trim() - if (!trimmed) { - throw new KeeperSdkError('Folder path cannot be empty.', 'folder_path_required') + const trimmed = path.trim(); + if (!trimmed) { + throw new KeeperSdkError( + "Folder path cannot be empty.", + "folder_path_required", + ); + } + + if (options.sharedFolder && options.userFolder) { + throw new KeeperSdkError( + "Use only one of sharedFolder (-sf) or userFolder (-uf).", + "mkdir_flags_conflict", + ); + } + + const { folderUid: baseUid, remaining } = await tryResolvePath( + storage, + session, + trimmed, + ); + if (!remaining.trim()) { + throw new KeeperSdkError( + `Folder "${trimmed}" already exists.`, + "folder_already_exists", + ); + } + + const grantAll = options.grantAll === true; + let manageUsers = isBoolean(options.manageUsers) + ? options.manageUsers + : false; + let manageRecords = isBoolean(options.manageRecords) + ? options.manageRecords + : false; + let canShare = isBoolean(options.canShare) ? options.canShare : false; + let canEdit = isBoolean(options.canEdit) ? options.canEdit : false; + if (grantAll) { + manageUsers = true; + manageRecords = true; + canShare = true; + canEdit = true; + } + + const segments = splitPathComponents(remaining) + .map((segment) => segment.trim()) + .filter((segment) => segment !== "" && segment !== "."); + + if (segments.length === 0) { + throw new KeeperSdkError( + `Folder "${trimmed}" already exists.`, + "folder_already_exists", + ); + } + + let currentParent: string | null = baseUid; + let lastResult: AddFolderResult = { + folderUid: "", + success: false, + message: "not run", + }; + + for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) { + const segment = segments[segmentIndex]; + const isLastSegment = segmentIndex === segments.length - 1; + const existingChildUid = await findChildFolderUidByName( + storage, + currentParent, + segment, + ); + if (existingChildUid) { + if (isLastSegment) { + throw new KeeperSdkError( + `Folder "${segment}" already exists.`, + "folder_already_exists", + ); + } + currentParent = existingChildUid; + continue; } - if (options.sharedFolder && options.userFolder) { - throw new KeeperSdkError('Use only one of sharedFolder (-sf) or userFolder (-uf).', 'mkdir_flags_conflict') + const createAsSharedFolder = isLastSegment && options.sharedFolder === true; + + const parentContext = resolveParentContext(storage, currentParent); + if ( + createAsSharedFolder && + parentContext.kind !== ParentFolderKind.UserFolder && + parentContext.kind !== ParentFolderKind.VirtualRoot + ) { + throw new KeeperSdkError( + "Shared folders can only be created under a personal folder.", + "shared_folder_invalid_parent", + ); } - const { folderUid: baseUid, remaining } = await tryResolvePath(storage, session, trimmed) - if (!remaining.trim()) { - throw new KeeperSdkError(`Folder "${trimmed}" already exists.`, 'folder_already_exists') - } - - const grantAll = options.grantAll === true - let manageUsers = isBoolean(options.manageUsers) ? options.manageUsers : false - let manageRecords = isBoolean(options.manageRecords) ? options.manageRecords : false - let canShare = isBoolean(options.canShare) ? options.canShare : false - let canEdit = isBoolean(options.canEdit) ? options.canEdit : false - if (grantAll) { - manageUsers = true - manageRecords = true - canShare = true - canEdit = true - } - - const segments = splitPathComponents(remaining) - .map((segment) => segment.trim()) - .filter((segment) => segment !== '' && segment !== '.') - - if (segments.length === 0) { - throw new KeeperSdkError(`Folder "${trimmed}" already exists.`, 'folder_already_exists') - } - - let currentParent: string | null = baseUid - let lastResult: AddFolderResult = { - folderUid: '', + lastResult = await addFolder(auth, storage, { + folderName: segment, + parentUid: currentParent, + isSharedFolder: createAsSharedFolder, + manageUsers: + isLastSegment && createAsSharedFolder ? manageUsers : undefined, + manageRecords: + isLastSegment && createAsSharedFolder ? manageRecords : undefined, + canShare: isLastSegment && createAsSharedFolder ? canShare : undefined, + canEdit: isLastSegment && createAsSharedFolder ? canEdit : undefined, + }); + + if (!lastResult.success) { + return { + folderUid: lastResult.folderUid, success: false, - message: 'not run', - } - - for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) { - const segment = segments[segmentIndex] - const isLastSegment = segmentIndex === segments.length - 1 - const existingChildUid = await findChildFolderUidByName(storage, currentParent, segment) - if (existingChildUid) { - if (isLastSegment) { - throw new KeeperSdkError(`Folder "${segment}" already exists.`, 'folder_already_exists') - } - currentParent = existingChildUid - continue - } - - const createAsSharedFolder = isLastSegment && options.sharedFolder === true - - const parentContext = resolveParentContext(storage, currentParent) - if ( - createAsSharedFolder && - parentContext.kind !== ParentFolderKind.UserFolder && - parentContext.kind !== ParentFolderKind.VirtualRoot - ) { - throw new KeeperSdkError( - 'Shared folders can only be created under a personal folder.', - 'shared_folder_invalid_parent' - ) - } - - lastResult = await addFolder(auth, storage, { - folderName: segment, - parentUid: currentParent, - isSharedFolder: createAsSharedFolder, - manageUsers: isLastSegment && createAsSharedFolder ? manageUsers : undefined, - manageRecords: isLastSegment && createAsSharedFolder ? manageRecords : undefined, - canShare: isLastSegment && createAsSharedFolder ? canShare : undefined, - canEdit: isLastSegment && createAsSharedFolder ? canEdit : undefined, - }) - - if (!lastResult.success) { - return { - folderUid: lastResult.folderUid, - success: false, - message: lastResult.message, - } - } - currentParent = lastResult.folderUid + message: lastResult.message, + }; } + currentParent = lastResult.folderUid; + } - return { folderUid: lastResult.folderUid, success: true } + return { folderUid: lastResult.folderUid, success: true }; } diff --git a/KeeperSdk/src/folders/changeDirectory.ts b/KeeperSdk/src/folders/changeDirectory.ts index 7f8aeb96..da9c5d22 100644 --- a/KeeperSdk/src/folders/changeDirectory.ts +++ b/KeeperSdk/src/folders/changeDirectory.ts @@ -1,217 +1,266 @@ -import type { DSharedFolder, DSharedFolderFolder, DUserFolder } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError } from '../utils' -import { listFolder, listRootUserFolders } from './listFolder' -import type { ListFolderFolderSimple } from './listFolder' -import { FolderKind, VaultObjectKind, sharedFolderFolderName, sharedFolderName, userFolderName } from './folderHelpers' +import type { + DSharedFolder, + DSharedFolderFolder, + DUserFolder, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { KeeperSdkError } from "../utils"; +import { listFolder, listRootUserFolders } from "./listFolder"; +import type { ListFolderFolderSimple } from "./listFolder"; +import { + FolderKind, + VaultObjectKind, + sharedFolderFolderName, + sharedFolderName, + userFolderName, +} from "./folderHelpers"; -const VAULT_ROOT_DISPLAY_NAME = 'My Vault' +const VAULT_ROOT_DISPLAY_NAME = "My Vault"; -const ESCAPED_SEPARATOR_PLACEHOLDER = '\x00' +const ESCAPED_SEPARATOR_PLACEHOLDER = "\x00"; export type VaultFolderSession = { - currentFolderUid: string | null -} + currentFolderUid: string | null; +}; export type ChangeDirectoryResult = { - folderUid: string | null - name: string -} + folderUid: string | null; + name: string; +}; export function createVaultFolderSession(): VaultFolderSession { - return { currentFolderUid: null } + return { currentFolderUid: null }; } -function getFolderEntryByUid(storage: InMemoryStorage, uid: string): ListFolderFolderSimple | undefined { - const userFolder = storage.getByUid(FolderKind.UserFolder, uid) - if (userFolder) { - return { uid: userFolder.uid, name: userFolderName(userFolder), folderKind: FolderKind.UserFolder } - } - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, uid) - if (sharedFolder) { - return { - uid: sharedFolder.uid, - name: sharedFolderName(sharedFolder), - folderKind: FolderKind.SharedFolder, - } - } - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, uid) - if (sharedFolderFolder) { - return { - uid: sharedFolderFolder.uid, - name: sharedFolderFolderName(sharedFolderFolder), - folderKind: FolderKind.SharedFolderFolder, - } - } - return undefined +function getFolderEntryByUid( + storage: InMemoryStorage, + uid: string, +): ListFolderFolderSimple | undefined { + const userFolder = storage.getByUid(FolderKind.UserFolder, uid); + if (userFolder) { + return { + uid: userFolder.uid, + name: userFolderName(userFolder), + folderKind: FolderKind.UserFolder, + }; + } + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + uid, + ); + if (sharedFolder) { + return { + uid: sharedFolder.uid, + name: sharedFolderName(sharedFolder), + folderKind: FolderKind.SharedFolder, + }; + } + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + uid, + ); + if (sharedFolderFolder) { + return { + uid: sharedFolderFolder.uid, + name: sharedFolderFolderName(sharedFolderFolder), + folderKind: FolderKind.SharedFolderFolder, + }; + } + return undefined; } -export async function findParentFolderUid(storage: InMemoryStorage, folderUid: string): Promise { - const rootFolderUids = new Set((await listRootUserFolders(storage)).map((folder) => folder.uid)) - if (rootFolderUids.has(folderUid)) return null - - const parentKinds = [ - FolderKind.UserFolder, - FolderKind.SharedFolder, - FolderKind.SharedFolderFolder, - VaultObjectKind.Team, - ] as const - for (const kind of parentKinds) { - for (const candidate of storage.getAll(kind)) { - const candidateUid = (candidate as { uid: string }).uid - const dependencies = (await storage.getDependencies(candidateUid)) || [] - for (const dependency of dependencies) { - if (dependency.uid !== folderUid) continue - if ( - dependency.kind === FolderKind.UserFolder || - dependency.kind === FolderKind.SharedFolder || - dependency.kind === FolderKind.SharedFolderFolder - ) { - return candidateUid - } - } +export async function findParentFolderUid( + storage: InMemoryStorage, + folderUid: string, +): Promise { + const rootFolderUids = new Set( + (await listRootUserFolders(storage)).map((folder) => folder.uid), + ); + if (rootFolderUids.has(folderUid)) return null; + + const parentKinds = [ + FolderKind.UserFolder, + FolderKind.SharedFolder, + FolderKind.SharedFolderFolder, + VaultObjectKind.Team, + ] as const; + for (const kind of parentKinds) { + for (const candidate of storage.getAll(kind)) { + const candidateUid = (candidate as { uid: string }).uid; + const dependencies = (await storage.getDependencies(candidateUid)) || []; + for (const dependency of dependencies) { + if (dependency.uid !== folderUid) continue; + if ( + dependency.kind === FolderKind.UserFolder || + dependency.kind === FolderKind.SharedFolder || + dependency.kind === FolderKind.SharedFolderFolder + ) { + return candidateUid; } + } } + } - return null + return null; } async function listFolderChildrenForCd( - storage: InMemoryStorage, - parentUid: string | null + storage: InMemoryStorage, + parentUid: string | null, ): Promise { - const result = await listFolder(storage, { - folderUid: parentUid === null ? undefined : parentUid, - showFolders: true, - showRecords: false, - }) - return result.folders + const result = await listFolder(storage, { + folderUid: parentUid === null ? undefined : parentUid, + showFolders: true, + showRecords: false, + }); + return result.folders; } function findChildFolder( - children: ListFolderFolderSimple[], - component: string + children: ListFolderFolderSimple[], + component: string, ): ListFolderFolderSimple | undefined { - const trimmedComponent = component.trim() - if (!trimmedComponent) return undefined - const matchByUid = children.find((child) => child.uid === trimmedComponent) - if (matchByUid) return matchByUid - const exactNameMatch = children.find((child) => child.name.trim() === trimmedComponent) - if (exactNameMatch) return exactNameMatch - const lowerComponent = trimmedComponent.toLowerCase() - return children.find((child) => child.name.trim().toLowerCase() === lowerComponent) + const trimmedComponent = component.trim(); + if (!trimmedComponent) return undefined; + const matchByUid = children.find((child) => child.uid === trimmedComponent); + if (matchByUid) return matchByUid; + const exactNameMatch = children.find( + (child) => child.name.trim() === trimmedComponent, + ); + if (exactNameMatch) return exactNameMatch; + const lowerComponent = trimmedComponent.toLowerCase(); + return children.find( + (child) => child.name.trim().toLowerCase() === lowerComponent, + ); } export function splitPathComponents(path: string): string[] { - const escaped = path.replace(/\/\//g, ESCAPED_SEPARATOR_PLACEHOLDER) - return escaped.split('/').map((segment) => segment.replace(/\x00/g, '/')) + const escaped = path.replace(/\/\//g, ESCAPED_SEPARATOR_PLACEHOLDER); + return escaped.split("/").map((segment) => segment.replace(/\x00/g, "/")); } export type TryResolvePathResult = { - folderUid: string | null - remaining: string -} + folderUid: string | null; + remaining: string; +}; export async function tryResolvePath( - storage: InMemoryStorage, - session: VaultFolderSession, - path: string + storage: InMemoryStorage, + session: VaultFolderSession, + path: string, ): Promise { - const trimmed = path.trim() - if (!trimmed) { - return { folderUid: session.currentFolderUid, remaining: '' } - } + const trimmed = path.trim(); + if (!trimmed) { + return { folderUid: session.currentFolderUid, remaining: "" }; + } - const direct = getFolderEntryByUid(storage, trimmed) - if (direct) { - return { folderUid: direct.uid, remaining: '' } - } + const direct = getFolderEntryByUid(storage, trimmed); + if (direct) { + return { folderUid: direct.uid, remaining: "" }; + } - const absolute = trimmed.startsWith('/') && !trimmed.startsWith('//') - const pathToWalk = absolute ? trimmed.slice(1) : trimmed - const components = splitPathComponents(pathToWalk) + const absolute = trimmed.startsWith("/") && !trimmed.startsWith("//"); + const pathToWalk = absolute ? trimmed.slice(1) : trimmed; + const components = splitPathComponents(pathToWalk); - let folderUid: string | null = absolute ? null : session.currentFolderUid || null + let folderUid: string | null = absolute + ? null + : session.currentFolderUid || null; - if (!absolute && folderUid !== null) { - if (!getFolderEntryByUid(storage, folderUid)) { - folderUid = null - } + if (!absolute && folderUid !== null) { + if (!getFolderEntryByUid(storage, folderUid)) { + folderUid = null; } + } - let componentIndex = 0 - while (componentIndex < components.length) { - const component = components[componentIndex] - componentIndex++ + let componentIndex = 0; + while (componentIndex < components.length) { + const component = components[componentIndex]; + componentIndex++; - if (component === '' || component === '.') { - continue - } + if (component === "" || component === ".") { + continue; + } - if (component === '..') { - if (folderUid === null) { - continue - } - folderUid = await findParentFolderUid(storage, folderUid) - continue - } + if (component === "..") { + if (folderUid === null) { + continue; + } + folderUid = await findParentFolderUid(storage, folderUid); + continue; + } - const children = await listFolderChildrenForCd(storage, folderUid) - const match = findChildFolder(children, component) - if (match) { - folderUid = match.uid - } else { - const remaining = [component, ...components.slice(componentIndex)].join('/') - return { folderUid, remaining } - } + const children = await listFolderChildrenForCd(storage, folderUid); + const match = findChildFolder(children, component); + if (match) { + folderUid = match.uid; + } else { + const remaining = [component, ...components.slice(componentIndex)].join( + "/", + ); + return { folderUid, remaining }; } + } - return { folderUid, remaining: '' } + return { folderUid, remaining: "" }; } export async function resolveSingleFolder( - storage: InMemoryStorage, - session: VaultFolderSession, - folderName: string + storage: InMemoryStorage, + session: VaultFolderSession, + folderName: string, ): Promise { - const trimmed = folderName.trim() - if (!trimmed) { - throw new KeeperSdkError('Folder cannot be empty.', 'folder_required') - } + const trimmed = folderName.trim(); + if (!trimmed) { + throw new KeeperSdkError("Folder cannot be empty.", "folder_required"); + } - const direct = getFolderEntryByUid(storage, trimmed) - if (direct) { - return { folderUid: direct.uid, name: direct.name } - } + const direct = getFolderEntryByUid(storage, trimmed); + if (direct) { + return { folderUid: direct.uid, name: direct.name }; + } - const { folderUid, remaining } = await tryResolvePath(storage, session, trimmed) - if (remaining) { - throw new KeeperSdkError(`Folder "${folderName}" not found`, 'folder_not_found') - } + const { folderUid, remaining } = await tryResolvePath( + storage, + session, + trimmed, + ); + if (remaining) { + throw new KeeperSdkError( + `Folder "${folderName}" not found`, + "folder_not_found", + ); + } - if (folderUid === null) { - return { folderUid: null, name: VAULT_ROOT_DISPLAY_NAME } - } + if (folderUid === null) { + return { folderUid: null, name: VAULT_ROOT_DISPLAY_NAME }; + } - const entry = getFolderEntryByUid(storage, folderUid) - if (!entry) { - throw new KeeperSdkError(`Folder "${folderName}" not found`, 'folder_not_found') - } - return { folderUid: entry.uid, name: entry.name } + const entry = getFolderEntryByUid(storage, folderUid); + if (!entry) { + throw new KeeperSdkError( + `Folder "${folderName}" not found`, + "folder_not_found", + ); + } + return { folderUid: entry.uid, name: entry.name }; } export async function changeDirectory( - storage: InMemoryStorage, - session: VaultFolderSession, - path: string + storage: InMemoryStorage, + session: VaultFolderSession, + path: string, ): Promise { - const resolved = await resolveSingleFolder(storage, session, path) - session.currentFolderUid = resolved.folderUid - return resolved + const resolved = await resolveSingleFolder(storage, session, path); + session.currentFolderUid = resolved.folderUid; + return resolved; } -export function getWorkingFolderDisplayName(storage: InMemoryStorage, currentFolderUid: string | null): string { - if (currentFolderUid === null) return VAULT_ROOT_DISPLAY_NAME - const entry = getFolderEntryByUid(storage, currentFolderUid) - return entry?.name || VAULT_ROOT_DISPLAY_NAME +export function getWorkingFolderDisplayName( + storage: InMemoryStorage, + currentFolderUid: string | null, +): string { + if (currentFolderUid === null) return VAULT_ROOT_DISPLAY_NAME; + const entry = getFolderEntryByUid(storage, currentFolderUid); + return entry?.name || VAULT_ROOT_DISPLAY_NAME; } diff --git a/KeeperSdk/src/folders/deleteFolder.ts b/KeeperSdk/src/folders/deleteFolder.ts index 93a512fe..a619c44d 100644 --- a/KeeperSdk/src/folders/deleteFolder.ts +++ b/KeeperSdk/src/folders/deleteFolder.ts @@ -1,293 +1,348 @@ -import type { Auth, DeleteObject, KeeperPreDeleteResponse } from '@keeper-security/keeperapi' -import { preDeleteCommand, recordDeleteCommand } from '@keeper-security/keeperapi' -import type { DSharedFolder, DSharedFolderFolder, DUserFolder } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError, extractErrorMessage, logger } from '../utils' -import { listFolder } from './listFolder' -import type { ListFolderFolderSimple } from './listFolder' -import { tryResolvePath, findParentFolderUid, type VaultFolderSession } from './changeDirectory' +import type { + Auth, + DeleteObject, + KeeperPreDeleteResponse, +} from "@keeper-security/keeperapi"; import { - DeleteResolution, - FolderKind, - globToRegex, - sharedFolderFolderName, - sharedFolderName, - userFolderName, -} from './folderHelpers' -import { findFolder } from './getFolder' + preDeleteCommand, + recordDeleteCommand, +} from "@keeper-security/keeperapi"; +import type { + DSharedFolder, + DSharedFolderFolder, + DUserFolder, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { KeeperSdkError, extractErrorMessage, logger } from "../utils"; +import { listFolder } from "./listFolder"; +import type { ListFolderFolderSimple } from "./listFolder"; +import { + tryResolvePath, + findParentFolderUid, + type VaultFolderSession, +} from "./changeDirectory"; +import { + DeleteResolution, + FolderKind, + globToRegex, + sharedFolderFolderName, + sharedFolderName, + userFolderName, +} from "./folderHelpers"; +import { findFolder } from "./getFolder"; export type DeleteFolderResult = { - success: boolean - message?: string - cancelled?: boolean -} + success: boolean; + message?: string; + cancelled?: boolean; +}; export type RmdirOptions = { - force?: boolean - quiet?: boolean - confirm?: (summary: string) => boolean | Promise -} + force?: boolean; + quiet?: boolean; + confirm?: (summary: string) => boolean | Promise; +}; function folderKindOfUid(storage: InMemoryStorage, uid: string): FolderKind { - if (storage.getByUid(FolderKind.UserFolder, uid)) return FolderKind.UserFolder - if (storage.getByUid(FolderKind.SharedFolder, uid)) return FolderKind.SharedFolder - if (storage.getByUid(FolderKind.SharedFolderFolder, uid)) return FolderKind.SharedFolderFolder - return FolderKind.UserFolder + if (storage.getByUid(FolderKind.UserFolder, uid)) + return FolderKind.UserFolder; + if (storage.getByUid(FolderKind.SharedFolder, uid)) + return FolderKind.SharedFolder; + if (storage.getByUid(FolderKind.SharedFolderFolder, uid)) + return FolderKind.SharedFolderFolder; + return FolderKind.UserFolder; } async function listFolderChildrenFolders( - storage: InMemoryStorage, - parentUid: string | null + storage: InMemoryStorage, + parentUid: string | null, ): Promise { - const result = await listFolder(storage, { - folderUid: parentUid === null ? undefined : parentUid, - showFolders: true, - showRecords: false, - }) - return result.folders + const result = await listFolder(storage, { + folderUid: parentUid === null ? undefined : parentUid, + showFolders: true, + showRecords: false, + }); + return result.folders; } function folderDisplayName(storage: InMemoryStorage, uid: string): string { - const userFolder = storage.getByUid(FolderKind.UserFolder, uid) - if (userFolder) return userFolderName(userFolder) - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, uid) - if (sharedFolder) return sharedFolderName(sharedFolder) - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, uid) - if (sharedFolderFolder) return sharedFolderFolderName(sharedFolderFolder) - return uid + const userFolder = storage.getByUid(FolderKind.UserFolder, uid); + if (userFolder) return userFolderName(userFolder); + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + uid, + ); + if (sharedFolder) return sharedFolderName(sharedFolder); + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + uid, + ); + if (sharedFolderFolder) return sharedFolderFolderName(sharedFolderFolder); + return uid; } export async function buildFolderDeleteObject( - storage: InMemoryStorage, - folderUid: string + storage: InMemoryStorage, + folderUid: string, ): Promise { - const userFolder = storage.getByUid(FolderKind.UserFolder, folderUid) - if (userFolder) { - const parentUid = await findParentFolderUid(storage, folderUid) - const deleteObject: DeleteObject = { - delete_resolution: DeleteResolution.Unlink, - object_uid: folderUid, - object_type: FolderKind.UserFolder, - from_type: FolderKind.UserFolder, - } - if (parentUid) { - deleteObject.from_uid = parentUid - deleteObject.from_type = folderKindOfUid(storage, parentUid) - } else { - deleteObject.from_uid = '' - } - return deleteObject + const userFolder = storage.getByUid( + FolderKind.UserFolder, + folderUid, + ); + if (userFolder) { + const parentUid = await findParentFolderUid(storage, folderUid); + const deleteObject: DeleteObject = { + delete_resolution: DeleteResolution.Unlink, + object_uid: folderUid, + object_type: FolderKind.UserFolder, + from_type: FolderKind.UserFolder, + }; + if (parentUid) { + deleteObject.from_uid = parentUid; + deleteObject.from_type = folderKindOfUid(storage, parentUid); + } else { + deleteObject.from_uid = ""; } - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, folderUid) - if (sharedFolder) { - const parentUid = await findParentFolderUid(storage, folderUid) - const deleteObject: DeleteObject = { - delete_resolution: DeleteResolution.Unlink, - object_uid: folderUid, - object_type: FolderKind.SharedFolder, - from_type: FolderKind.UserFolder, - } - if (parentUid) { - deleteObject.from_uid = parentUid - deleteObject.from_type = folderKindOfUid(storage, parentUid) - } else { - deleteObject.from_uid = '' - } - return deleteObject + return deleteObject; + } + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + folderUid, + ); + if (sharedFolder) { + const parentUid = await findParentFolderUid(storage, folderUid); + const deleteObject: DeleteObject = { + delete_resolution: DeleteResolution.Unlink, + object_uid: folderUid, + object_type: FolderKind.SharedFolder, + from_type: FolderKind.UserFolder, + }; + if (parentUid) { + deleteObject.from_uid = parentUid; + deleteObject.from_type = folderKindOfUid(storage, parentUid); + } else { + deleteObject.from_uid = ""; } - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, folderUid) - if (sharedFolderFolder) { - const parentUid = await findParentFolderUid(storage, folderUid) - const deleteObject: DeleteObject = { - delete_resolution: DeleteResolution.Unlink, - object_uid: folderUid, - object_type: FolderKind.SharedFolderFolder, - from_type: FolderKind.SharedFolder, - } - if (parentUid) { - deleteObject.from_uid = parentUid - deleteObject.from_type = folderKindOfUid(storage, parentUid) - } else { - deleteObject.from_uid = sharedFolderFolder.sharedFolderUid - deleteObject.from_type = FolderKind.SharedFolder - } - return deleteObject + return deleteObject; + } + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + folderUid, + ); + if (sharedFolderFolder) { + const parentUid = await findParentFolderUid(storage, folderUid); + const deleteObject: DeleteObject = { + delete_resolution: DeleteResolution.Unlink, + object_uid: folderUid, + object_type: FolderKind.SharedFolderFolder, + from_type: FolderKind.SharedFolder, + }; + if (parentUid) { + deleteObject.from_uid = parentUid; + deleteObject.from_type = folderKindOfUid(storage, parentUid); + } else { + deleteObject.from_uid = sharedFolderFolder.sharedFolderUid; + deleteObject.from_type = FolderKind.SharedFolder; } - return null + return deleteObject; + } + return null; } async function buildDeleteObjectForFolderRef( - storage: InMemoryStorage, - ref: string + storage: InMemoryStorage, + ref: string, ): Promise { - const trimmed = ref.trim() - if (!trimmed) return null + const trimmed = ref.trim(); + if (!trimmed) return null; - const direct = await buildFolderDeleteObject(storage, trimmed) - if (direct) return direct + const direct = await buildFolderDeleteObject(storage, trimmed); + if (direct) return direct; - const found = findFolder(storage, trimmed) - if (!found) return null - return buildFolderDeleteObject(storage, found.folder.uid) + const found = findFolder(storage, trimmed); + if (!found) return null; + return buildFolderDeleteObject(storage, found.folder.uid); } export async function deleteFolder( - auth: Auth, - storage: InMemoryStorage, - folderRefs: string[], - confirm?: (summary: string) => boolean | Promise + auth: Auth, + storage: InMemoryStorage, + folderRefs: string[], + confirm?: (summary: string) => boolean | Promise, ): Promise { - const objects: DeleteObject[] = [] - for (const ref of folderRefs) { - const deleteObject = await buildDeleteObjectForFolderRef(storage, ref) - if (deleteObject) objects.push(deleteObject) - } - if (objects.length === 0) { - throw new KeeperSdkError( - 'No folders found to delete (not a folder UID or name).', - 'delete_nothing' - ) - } + const objects: DeleteObject[] = []; + for (const ref of folderRefs) { + const deleteObject = await buildDeleteObjectForFolderRef(storage, ref); + if (deleteObject) objects.push(deleteObject); + } + if (objects.length === 0) { + throw new KeeperSdkError( + "No folders found to delete (not a folder UID or name).", + "delete_nothing", + ); + } - const targetUids = objects.map((deleteObject) => deleteObject.object_uid).join(', ') + const targetUids = objects + .map((deleteObject) => deleteObject.object_uid) + .join(", "); - let preResp: KeeperPreDeleteResponse - try { - preResp = await auth.executeRestCommand(preDeleteCommand({ objects })) - } catch (err) { - return { - success: false, - message: `pre_delete failed for [${targetUids}]: ${extractErrorMessage(err)}`, - } - } + let preResp: KeeperPreDeleteResponse; + try { + preResp = await auth.executeRestCommand(preDeleteCommand({ objects })); + } catch (err) { + return { + success: false, + message: `pre_delete failed for [${targetUids}]: ${extractErrorMessage(err)}`, + }; + } - const inner = preResp.pre_delete_response - const token = inner?.pre_delete_token - if (!token) { - const reason = - preResp.message || - preResp.result_code || - `pre_delete failed for [${targetUids}]: server did not return a pre_delete_token` - return { - success: false, - message: reason, - } - } + const inner = preResp.pre_delete_response; + const token = inner?.pre_delete_token; + if (!token) { + const reason = + preResp.message || + preResp.result_code || + `pre_delete failed for [${targetUids}]: server did not return a pre_delete_token`; + return { + success: false, + message: reason, + }; + } - if (confirm) { - const wouldDelete = inner?.would_delete - const summaryItems = wouldDelete?.deletion_summary - if (Array.isArray(summaryItems) && summaryItems.length > 0) { - const summary = summaryItems.join('\n') - const confirmed = await confirm(summary) - if (!confirmed) { - return { success: false, cancelled: true, message: 'Cancelled.' } - } - } + if (confirm) { + const wouldDelete = inner?.would_delete; + const summaryItems = wouldDelete?.deletion_summary; + if (Array.isArray(summaryItems) && summaryItems.length > 0) { + const summary = summaryItems.join("\n"); + const confirmed = await confirm(summary); + if (!confirmed) { + return { success: false, cancelled: true, message: "Cancelled." }; + } } + } - try { - await auth.executeRestCommand(recordDeleteCommand({ pre_delete_token: token })) - } catch (err) { - return { - success: false, - message: `record_delete failed for [${targetUids}]: ${extractErrorMessage(err)}`, - } - } + try { + await auth.executeRestCommand( + recordDeleteCommand({ pre_delete_token: token }), + ); + } catch (err) { + return { + success: false, + message: `record_delete failed for [${targetUids}]: ${extractErrorMessage(err)}`, + }; + } - return { success: true } + return { success: true }; } export async function resolveRmdirPatternsToFolderUids( - storage: InMemoryStorage, - session: VaultFolderSession, - pattern: string + storage: InMemoryStorage, + session: VaultFolderSession, + pattern: string, ): Promise> { - const matchedUids = new Set() - const trimmedPattern = pattern.trim() - if (!trimmedPattern) return matchedUids + const matchedUids = new Set(); + const trimmedPattern = pattern.trim(); + if (!trimmedPattern) return matchedUids; - const { folderUid: baseUid, remaining } = await tryResolvePath(storage, session, trimmedPattern) - const remainingPattern = remaining.trim() + const { folderUid: baseUid, remaining } = await tryResolvePath( + storage, + session, + trimmedPattern, + ); + const remainingPattern = remaining.trim(); - if (!remainingPattern) { - if (baseUid !== null) { - matchedUids.add(baseUid) - } - return matchedUids + if (!remainingPattern) { + if (baseUid !== null) { + matchedUids.add(baseUid); } + return matchedUids; + } - const children = await listFolderChildrenFolders(storage, baseUid) - const exactChild = children.find((child) => child.uid === remainingPattern) - if (exactChild) { - matchedUids.add(exactChild.uid) - return matchedUids - } + const children = await listFolderChildrenFolders(storage, baseUid); + const exactChild = children.find((child) => child.uid === remainingPattern); + if (exactChild) { + matchedUids.add(exactChild.uid); + return matchedUids; + } - const matcher = globToRegex(remainingPattern) - for (const child of children) { - if (matcher.test(child.name.trim())) { - matchedUids.add(child.uid) - } + const matcher = globToRegex(remainingPattern); + for (const child of children) { + if (matcher.test(child.name.trim())) { + matchedUids.add(child.uid); } - return matchedUids + } + return matchedUids; } -async function dedupeNestedFolderDeletes(storage: InMemoryStorage, folderUids: Set): Promise { - for (const folderUid of [...folderUids]) { - let current = folderUid - while (true) { - const parentUid = await findParentFolderUid(storage, current) - if (!parentUid) break - if (folderUids.has(parentUid)) { - folderUids.delete(folderUid) - break - } - current = parentUid - } +async function dedupeNestedFolderDeletes( + storage: InMemoryStorage, + folderUids: Set, +): Promise { + for (const folderUid of [...folderUids]) { + let current = folderUid; + while (true) { + const parentUid = await findParentFolderUid(storage, current); + if (!parentUid) break; + if (folderUids.has(parentUid)) { + folderUids.delete(folderUid); + break; + } + current = parentUid; } + } } export async function rmdir( - auth: Auth, - storage: InMemoryStorage, - session: VaultFolderSession, - patterns: string[], - options: RmdirOptions = {} + auth: Auth, + storage: InMemoryStorage, + session: VaultFolderSession, + patterns: string[], + options: RmdirOptions = {}, ): Promise { - const { force = false, quiet = false, confirm } = options - if (!force && !confirm) { - throw new KeeperSdkError( - 'Confirmation is required: pass `confirm` or set `force: true`.', - 'rmdir_confirm_required' - ) - } - const folderUids = new Set() + const { force = false, quiet = false, confirm } = options; + if (!force && !confirm) { + throw new KeeperSdkError( + "Confirmation is required: pass `confirm` or set `force: true`.", + "rmdir_confirm_required", + ); + } + const folderUids = new Set(); - for (const pattern of patterns) { - const trimmedPattern = pattern.trim() - if (!trimmedPattern) continue - const resolved = await resolveRmdirPatternsToFolderUids(storage, session, trimmedPattern) - for (const matchedUid of resolved) { - folderUids.add(matchedUid) - } + for (const pattern of patterns) { + const trimmedPattern = pattern.trim(); + if (!trimmedPattern) continue; + const resolved = await resolveRmdirPatternsToFolderUids( + storage, + session, + trimmedPattern, + ); + for (const matchedUid of resolved) { + folderUids.add(matchedUid); } + } - if (folderUids.size === 0) { - throw new KeeperSdkError('Enter name of an existing folder.', 'rmdir_no_match') - } + if (folderUids.size === 0) { + throw new KeeperSdkError( + "Enter name of an existing folder.", + "rmdir_no_match", + ); + } - await dedupeNestedFolderDeletes(storage, folderUids) + await dedupeNestedFolderDeletes(storage, folderUids); - const sortedNames = [...folderUids] - .map((uid) => folderDisplayName(storage, uid)) - .sort((nameA, nameB) => nameA.localeCompare(nameB, undefined, { sensitivity: 'base' })) + const sortedNames = [...folderUids] + .map((uid) => folderDisplayName(storage, uid)) + .sort((nameA, nameB) => + nameA.localeCompare(nameB, undefined, { sensitivity: "base" }), + ); - if (!quiet || !force) { - logger.info(`\nThe following folder(s) will be removed:\n${sortedNames.join(', ')}\n`) - } + if (!quiet || !force) { + logger.info( + `\nThe following folder(s) will be removed:\n${sortedNames.join(", ")}\n`, + ); + } - const confirmFn = force ? undefined : confirm + const confirmFn = force ? undefined : confirm; - return deleteFolder(auth, storage, [...folderUids], confirmFn) + return deleteFolder(auth, storage, [...folderUids], confirmFn); } diff --git a/KeeperSdk/src/folders/folderHelpers.ts b/KeeperSdk/src/folders/folderHelpers.ts index 618ad342..addb8e0f 100644 --- a/KeeperSdk/src/folders/folderHelpers.ts +++ b/KeeperSdk/src/folders/folderHelpers.ts @@ -1,107 +1,118 @@ -import type { DSharedFolder, DSharedFolderFolder, DUserFolder } from '@keeper-security/keeperapi' -import type { InMemoryStorage } from '../storage/InMemoryStorage' -import { escapeRegExp, KeeperSdkError } from '../utils' +import type { + DSharedFolder, + DSharedFolderFolder, + DUserFolder, +} from "@keeper-security/keeperapi"; +import type { InMemoryStorage } from "../storage/InMemoryStorage"; +import { escapeRegExp, KeeperSdkError } from "../utils"; -export const MAX_FOLDER_NAME_LENGTH = 255 +export const MAX_FOLDER_NAME_LENGTH = 255; export function validateFolderName(name: string): void { - if (name.length > MAX_FOLDER_NAME_LENGTH) { - throw new KeeperSdkError(`Folder name exceeds ${MAX_FOLDER_NAME_LENGTH} characters.`, 'folder_name_too_long') - } + if (name.length > MAX_FOLDER_NAME_LENGTH) { + throw new KeeperSdkError( + `Folder name exceeds ${MAX_FOLDER_NAME_LENGTH} characters.`, + "folder_name_too_long", + ); + } } export enum FolderKind { - UserFolder = 'user_folder', - SharedFolder = 'shared_folder', - SharedFolderFolder = 'shared_folder_folder', + UserFolder = "user_folder", + SharedFolder = "shared_folder", + SharedFolderFolder = "shared_folder_folder", } export enum ParentFolderKind { - VirtualRoot = 'virtual_root', - UserFolder = 'user_folder', - SharedFolder = 'shared_folder', - SharedFolderFolder = 'shared_folder_folder', + VirtualRoot = "virtual_root", + UserFolder = "user_folder", + SharedFolder = "shared_folder", + SharedFolderFolder = "shared_folder_folder", } export enum FolderObjectType { - Folder = 'folder', - SharedFolder = 'shared_folder', + Folder = "folder", + SharedFolder = "shared_folder", } export enum DeleteResolution { - Unlink = 'unlink', + Unlink = "unlink", } export enum DeleteObjectType { - Record = 'record', - UserFolder = 'user_folder', - SharedFolder = 'shared_folder', - SharedFolderFolder = 'shared_folder_folder', + Record = "record", + UserFolder = "user_folder", + SharedFolder = "shared_folder", + SharedFolderFolder = "shared_folder_folder", } export enum FolderResultStatus { - Success = 'success', - Invited = 'invited', - Unknown = 'unknown', + Success = "success", + Invited = "invited", + Unknown = "unknown", } export enum VaultObjectKind { - Record = 'record', - Metadata = 'metadata', - NonSharedData = 'non_shared_data', - Team = 'team', - User = 'user', - SharedFolderUser = 'shared_folder_user', - SharedFolderTeam = 'shared_folder_team', - SharedFolderRecord = 'shared_folder_record', + Record = "record", + Metadata = "metadata", + NonSharedData = "non_shared_data", + Team = "team", + User = "user", + SharedFolderUser = "shared_folder_user", + SharedFolderTeam = "shared_folder_team", + SharedFolderRecord = "shared_folder_record", } -export type FolderKindOrLiteral = FolderKind | `${FolderKind}` - -export function folderKindFromString(value: string | undefined | null): FolderKind | undefined { - if (!value) return undefined - switch (value) { - case FolderKind.UserFolder: - return FolderKind.UserFolder - case FolderKind.SharedFolder: - return FolderKind.SharedFolder - case FolderKind.SharedFolderFolder: - return FolderKind.SharedFolderFolder - default: - return undefined - } +export type FolderKindOrLiteral = FolderKind | `${FolderKind}`; + +export function folderKindFromString( + value: string | undefined | null, +): FolderKind | undefined { + if (!value) return undefined; + switch (value) { + case FolderKind.UserFolder: + return FolderKind.UserFolder; + case FolderKind.SharedFolder: + return FolderKind.SharedFolder; + case FolderKind.SharedFolderFolder: + return FolderKind.SharedFolderFolder; + default: + return undefined; + } } export function userFolderName(folder: DUserFolder): string { - const data = folder.data as { title?: string; name?: string } | undefined - return (data?.title || data?.name || folder.uid).trim() || folder.uid + const data = folder.data as { title?: string; name?: string } | undefined; + return (data?.title || data?.name || folder.uid).trim() || folder.uid; } export function sharedFolderFolderName(folder: DSharedFolderFolder): string { - const data = folder.data as { title?: string; name?: string } | undefined - return (data?.title || data?.name || folder.uid).trim() || folder.uid + const data = folder.data as { title?: string; name?: string } | undefined; + return (data?.title || data?.name || folder.uid).trim() || folder.uid; } export function sharedFolderName(folder: DSharedFolder): string { - return (folder.name || folder.uid).trim() || folder.uid + return (folder.name || folder.uid).trim() || folder.uid; } export function globToRegex(pattern: string): RegExp { - const escapedPattern = escapeRegExp(pattern) - const regexBody = escapedPattern.replace(/\*/g, '.*').replace(/\?/g, '.') - return new RegExp(`^${regexBody}$`, 'i') + const escapedPattern = escapeRegExp(pattern); + const regexBody = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, "."); + return new RegExp(`^${regexBody}$`, "i"); } -export async function getUserFolderParentMap(storage: InMemoryStorage): Promise> { - const userFolders = storage.getAll(FolderKind.UserFolder) - const childToParent = new Map() - for (const userFolder of userFolders) { - const dependencies = (await storage.getDependencies(userFolder.uid)) || [] - for (const dependency of dependencies) { - if (dependency.kind === FolderKind.UserFolder) { - childToParent.set(dependency.uid, userFolder.uid) - } - } +export async function getUserFolderParentMap( + storage: InMemoryStorage, +): Promise> { + const userFolders = storage.getAll(FolderKind.UserFolder); + const childToParent = new Map(); + for (const userFolder of userFolders) { + const dependencies = (await storage.getDependencies(userFolder.uid)) || []; + for (const dependency of dependencies) { + if (dependency.kind === FolderKind.UserFolder) { + childToParent.set(dependency.uid, userFolder.uid); + } } - return childToParent + } + return childToParent; } diff --git a/KeeperSdk/src/folders/folderTree.ts b/KeeperSdk/src/folders/folderTree.ts index be047709..68bb23b3 100644 --- a/KeeperSdk/src/folders/folderTree.ts +++ b/KeeperSdk/src/folders/folderTree.ts @@ -1,344 +1,438 @@ import type { - DRecord, - DSharedFolder, - DSharedFolderFolder, - DSharedFolderTeam, - DSharedFolderUser, - DUser, - DUserFolder, -} from '@keeper-security/keeperapi' -import { webSafe64FromBytes } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { getRecordTitle } from '../records/RecordUtils' -import { listFolder, listVaultRootFolders } from './listFolder' -import { resolveSingleFolder, type VaultFolderSession } from './changeDirectory' -import { FolderKind, VaultObjectKind, sharedFolderFolderName, sharedFolderName, userFolderName } from './folderHelpers' + DRecord, + DSharedFolder, + DSharedFolderFolder, + DSharedFolderTeam, + DSharedFolderUser, + DUser, + DUserFolder, +} from "@keeper-security/keeperapi"; +import { webSafe64FromBytes } from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { getRecordTitle } from "../records/RecordUtils"; +import { listFolder, listVaultRootFolders } from "./listFolder"; +import { + resolveSingleFolder, + type VaultFolderSession, +} from "./changeDirectory"; +import { + FolderKind, + VaultObjectKind, + sharedFolderFolderName, + sharedFolderName, + userFolderName, +} from "./folderHelpers"; enum TreeItemKind { - Permission = 'perm', - Record = 'record', - Folder = 'folder', + Permission = "perm", + Record = "record", + Folder = "folder", } const TREE_TAG = { - folder: '[folder]', - sharedFolder: '[shared folder]', - record: '[record]', -} as const + folder: "[folder]", + sharedFolder: "[shared folder]", + record: "[record]", +} as const; export type FolderTreeBuildOptions = { - folderPath?: string | null - verbose?: boolean - showRecords?: boolean - showShares?: boolean - hideSharesKey?: boolean - title?: string | null -} + folderPath?: string | null; + verbose?: boolean; + showRecords?: boolean; + showShares?: boolean; + hideSharesKey?: boolean; + title?: string | null; +}; export type FolderTreeNode = { - displayName: string - children: FolderTreeNode[] - permissions?: { display: string }[] - records?: { display: string }[] -} + displayName: string; + children: FolderTreeNode[]; + permissions?: { display: string }[]; + records?: { display: string }[]; +}; export type FolderTreeResult = { - title?: string | null - root: FolderTreeNode -} + title?: string | null; + root: FolderTreeNode; +}; -export function userPermissionToText(manageUsers: boolean, manageRecords: boolean): string { - if (manageUsers && manageRecords) return 'Can Manage Users & Records' - if (manageUsers) return 'Can Manage Users' - if (manageRecords) return 'Can Manage Records' - return 'No User Permissions' +export function userPermissionToText( + manageUsers: boolean, + manageRecords: boolean, +): string { + if (manageUsers && manageRecords) return "Can Manage Users & Records"; + if (manageUsers) return "Can Manage Users"; + if (manageRecords) return "Can Manage Records"; + return "No User Permissions"; } -export function recordPermissionToText(canEdit: boolean, canShare: boolean): string { - if (canEdit && canShare) return 'Can Edit & Share' - if (canEdit) return 'Can Edit' - if (canShare) return 'Can Share' - return 'View Only' +export function recordPermissionToText( + canEdit: boolean, + canShare: boolean, +): string { + if (canEdit && canShare) return "Can Edit & Share"; + if (canEdit) return "Can Edit"; + if (canShare) return "Can Share"; + return "View Only"; } -function buildAccountUidEmailMap(storage: InMemoryStorage): Map { - const accountUidToEmail = new Map() - for (const user of storage.getAll(VaultObjectKind.User)) { - const uid = user.accountUid ? webSafe64FromBytes(user.accountUid) : '' - const email = (user.username || '').trim() - if (uid && email) accountUidToEmail.set(uid, email) - } - return accountUidToEmail +function buildAccountUidEmailMap( + storage: InMemoryStorage, +): Map { + const accountUidToEmail = new Map(); + for (const user of storage.getAll(VaultObjectKind.User)) { + const uid = user.accountUid ? webSafe64FromBytes(user.accountUid) : ""; + const email = (user.username || "").trim(); + if (uid && email) accountUidToEmail.set(uid, email); + } + return accountUidToEmail; } function resolveUserDisplayName( - accountUid: string | undefined, - accountUsername: string | undefined, - emailMap: Map + accountUid: string | undefined, + accountUsername: string | undefined, + emailMap: Map, ): string { - const explicit = (accountUsername || '').trim() - if (explicit) return explicit - if (accountUid) { - const fromMap = emailMap.get(accountUid) - if (fromMap) return fromMap - return accountUid - } - return 'unknown' + const explicit = (accountUsername || "").trim(); + if (explicit) return explicit; + if (accountUid) { + const fromMap = emailMap.get(accountUid); + if (fromMap) return fromMap; + return accountUid; + } + return "unknown"; } async function collectSharedFolderPermissions( - storage: InMemoryStorage, - sharedFolder: DSharedFolder, - verbose: boolean, - hideSharesKey: boolean, - emailMap: Map + storage: InMemoryStorage, + sharedFolder: DSharedFolder, + verbose: boolean, + hideSharesKey: boolean, + emailMap: Map, ): Promise<{ display: string }[]> { - const rows: { display: string; sortKey: string }[] = [] - const seenUserUids = new Set() - - for (const sharedUser of storage.getAll(VaultObjectKind.SharedFolderUser)) { - if (sharedUser.sharedFolderUid !== sharedFolder.uid) continue - if (sharedUser.accountUid) seenUserUids.add(sharedUser.accountUid) - const permissionText = userPermissionToText(sharedUser.manageUsers, sharedUser.manageRecords) - let name = resolveUserDisplayName(sharedUser.accountUid, sharedUser.accountUsername, emailMap) - if (verbose && sharedUser.accountUid) name += ` (${sharedUser.accountUid})` - const suffix = hideSharesKey ? '' : ` [User]` - rows.push({ - display: `${name}: ${permissionText}${suffix}`, - sortKey: name.toLowerCase(), - }) - } - - if (sharedFolder.ownerAccountUid && !seenUserUids.has(sharedFolder.ownerAccountUid)) { - const ownerName = resolveUserDisplayName(sharedFolder.ownerAccountUid, sharedFolder.ownerUsername, emailMap) - let display = ownerName - if (verbose) display += ` (${sharedFolder.ownerAccountUid})` - const suffix = hideSharesKey ? '' : ` [User]` - rows.push({ - display: `${display}: ${userPermissionToText(true, true)}${suffix}`, - sortKey: ownerName.toLowerCase(), - }) - } - - for (const sharedTeam of storage.getAll(VaultObjectKind.SharedFolderTeam)) { - if (sharedTeam.sharedFolderUid !== sharedFolder.uid) continue - const permissionText = userPermissionToText(sharedTeam.manageUsers, sharedTeam.manageRecords) - let name = sharedTeam.name || `(${sharedTeam.teamUid})` - if (verbose && sharedTeam.teamUid) name += ` (${sharedTeam.teamUid})` - const suffix = hideSharesKey ? '' : ` [Team]` - rows.push({ - display: `${name}: ${permissionText}${suffix}`, - sortKey: name.toLowerCase(), - }) - } - - rows.sort((rowA, rowB) => rowA.sortKey.localeCompare(rowB.sortKey)) - return rows.map((row) => ({ display: row.display })) + const rows: { display: string; sortKey: string }[] = []; + const seenUserUids = new Set(); + + for (const sharedUser of storage.getAll( + VaultObjectKind.SharedFolderUser, + )) { + if (sharedUser.sharedFolderUid !== sharedFolder.uid) continue; + if (sharedUser.accountUid) seenUserUids.add(sharedUser.accountUid); + const permissionText = userPermissionToText( + sharedUser.manageUsers, + sharedUser.manageRecords, + ); + let name = resolveUserDisplayName( + sharedUser.accountUid, + sharedUser.accountUsername, + emailMap, + ); + if (verbose && sharedUser.accountUid) name += ` (${sharedUser.accountUid})`; + const suffix = hideSharesKey ? "" : ` [User]`; + rows.push({ + display: `${name}: ${permissionText}${suffix}`, + sortKey: name.toLowerCase(), + }); + } + + if ( + sharedFolder.ownerAccountUid && + !seenUserUids.has(sharedFolder.ownerAccountUid) + ) { + const ownerName = resolveUserDisplayName( + sharedFolder.ownerAccountUid, + sharedFolder.ownerUsername, + emailMap, + ); + let display = ownerName; + if (verbose) display += ` (${sharedFolder.ownerAccountUid})`; + const suffix = hideSharesKey ? "" : ` [User]`; + rows.push({ + display: `${display}: ${userPermissionToText(true, true)}${suffix}`, + sortKey: ownerName.toLowerCase(), + }); + } + + for (const sharedTeam of storage.getAll( + VaultObjectKind.SharedFolderTeam, + )) { + if (sharedTeam.sharedFolderUid !== sharedFolder.uid) continue; + const permissionText = userPermissionToText( + sharedTeam.manageUsers, + sharedTeam.manageRecords, + ); + let name = sharedTeam.name || `(${sharedTeam.teamUid})`; + if (verbose && sharedTeam.teamUid) name += ` (${sharedTeam.teamUid})`; + const suffix = hideSharesKey ? "" : ` [Team]`; + rows.push({ + display: `${name}: ${permissionText}${suffix}`, + sortKey: name.toLowerCase(), + }); + } + + rows.sort((rowA, rowB) => rowA.sortKey.localeCompare(rowB.sortKey)); + return rows.map((row) => ({ display: row.display })); } function folderTreeTag( - userFolder: DUserFolder | undefined, - sharedFolder: DSharedFolder | undefined, - _sharedFolderFolder: DSharedFolderFolder | undefined + userFolder: DUserFolder | undefined, + sharedFolder: DSharedFolder | undefined, + _sharedFolderFolder: DSharedFolderFolder | undefined, ): string { - if (sharedFolder) return TREE_TAG.sharedFolder - if (userFolder) return TREE_TAG.folder - return TREE_TAG.folder + if (sharedFolder) return TREE_TAG.sharedFolder; + if (userFolder) return TREE_TAG.folder; + return TREE_TAG.folder; } -function formatTreeNodeName(baseName: string, tag: string, verbose: boolean, uid?: string): string { - const name = verbose && uid ? `${baseName} (${uid})` : baseName - return `${name} ${tag}` +function formatTreeNodeName( + baseName: string, + tag: string, + verbose: boolean, + uid?: string, +): string { + const name = verbose && uid ? `${baseName} (${uid})` : baseName; + return `${name} ${tag}`; } -function formatTreeRecordName(title: string, verbose: boolean, recordUid?: string): string { - const name = verbose && recordUid ? `${title} (${recordUid})` : title - return `${name} ${TREE_TAG.record}` +function formatTreeRecordName( + title: string, + verbose: boolean, + recordUid?: string, +): string { + const name = verbose && recordUid ? `${title} (${recordUid})` : title; + return `${name} ${TREE_TAG.record}`; } -type BuildOpts = Required> & { - promotedRootSharedUids?: Set - accountUidEmailMap: Map -} +type BuildOpts = Required< + Pick< + FolderTreeBuildOptions, + "verbose" | "showRecords" | "showShares" | "hideSharesKey" + > +> & { + promotedRootSharedUids?: Set; + accountUidEmailMap: Map; +}; async function buildFolderSubtree( - storage: InMemoryStorage, - folderUid: string, - opts: BuildOpts + storage: InMemoryStorage, + folderUid: string, + opts: BuildOpts, ): Promise { - const userFolder = storage.getByUid(FolderKind.UserFolder, folderUid) - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, folderUid) - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, folderUid) - if (!userFolder && !sharedFolder && !sharedFolderFolder) { - return { displayName: `(missing folder ${folderUid})`, children: [] } - } - - let baseName: string - if (userFolder) baseName = userFolderName(userFolder) - else if (sharedFolder) baseName = sharedFolderName(sharedFolder) - else baseName = sharedFolderFolderName(sharedFolderFolder!) - - let displayName = formatTreeNodeName( - baseName, - folderTreeTag(userFolder, sharedFolder, sharedFolderFolder), - opts.verbose, - folderUid - ) - - const node: FolderTreeNode = { displayName, children: [] } - - if (opts.showShares && sharedFolder) { - node.permissions = await collectSharedFolderPermissions( - storage, - sharedFolder, - opts.verbose, - opts.hideSharesKey, - opts.accountUidEmailMap - ) - } - - const listed = await listFolder(storage, { - folderUid: folderUid, - showFolders: true, - showRecords: opts.showRecords, - }) - - for (const childFolder of listed.folders) { - if (opts.promotedRootSharedUids?.has(childFolder.uid)) continue - node.children.push(await buildFolderSubtree(storage, childFolder.uid, opts)) - } - - if (opts.showRecords && 'records' in listed) { - const records = listed.records - node.records = records.map((recordRow) => { - const record = storage.getByUid(VaultObjectKind.Record, recordRow.uid) - const title = record ? getRecordTitle(record) : recordRow.name - return { display: formatTreeRecordName(title, opts.verbose, recordRow.uid) } - }) - } - - return node + const userFolder = storage.getByUid( + FolderKind.UserFolder, + folderUid, + ); + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + folderUid, + ); + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + folderUid, + ); + if (!userFolder && !sharedFolder && !sharedFolderFolder) { + return { displayName: `(missing folder ${folderUid})`, children: [] }; + } + + let baseName: string; + if (userFolder) baseName = userFolderName(userFolder); + else if (sharedFolder) baseName = sharedFolderName(sharedFolder); + else baseName = sharedFolderFolderName(sharedFolderFolder!); + + let displayName = formatTreeNodeName( + baseName, + folderTreeTag(userFolder, sharedFolder, sharedFolderFolder), + opts.verbose, + folderUid, + ); + + const node: FolderTreeNode = { displayName, children: [] }; + + if (opts.showShares && sharedFolder) { + node.permissions = await collectSharedFolderPermissions( + storage, + sharedFolder, + opts.verbose, + opts.hideSharesKey, + opts.accountUidEmailMap, + ); + } + + const listed = await listFolder(storage, { + folderUid: folderUid, + showFolders: true, + showRecords: opts.showRecords, + }); + + for (const childFolder of listed.folders) { + if (opts.promotedRootSharedUids?.has(childFolder.uid)) continue; + node.children.push( + await buildFolderSubtree(storage, childFolder.uid, opts), + ); + } + + if (opts.showRecords && "records" in listed) { + const records = listed.records; + node.records = records.map((recordRow) => { + const record = storage.getByUid( + VaultObjectKind.Record, + recordRow.uid, + ); + const title = record ? getRecordTitle(record) : recordRow.name; + return { + display: formatTreeRecordName(title, opts.verbose, recordRow.uid), + }; + }); + } + + return node; } -async function buildVaultRootTree(storage: InMemoryStorage, opts: BuildOpts): Promise { - const node: FolderTreeNode = { displayName: '', children: [] } - const { rows, promotedRootSharedUids } = await listVaultRootFolders(storage) - const optsWithPromoted: BuildOpts = { ...opts, promotedRootSharedUids } - for (const folderRow of rows) { - node.children.push(await buildFolderSubtree(storage, folderRow.uid, optsWithPromoted)) - } - const listed = await listFolder(storage, { - folderUid: undefined, - showFolders: true, - showRecords: opts.showRecords, - }) - if (opts.showRecords && listed.records?.length) { - node.records = listed.records.map((recordRow) => { - const record = storage.getByUid(VaultObjectKind.Record, recordRow.uid) - const title = record ? getRecordTitle(record) : recordRow.name - return { display: formatTreeRecordName(title, opts.verbose, recordRow.uid) } - }) - } - return node +async function buildVaultRootTree( + storage: InMemoryStorage, + opts: BuildOpts, +): Promise { + const node: FolderTreeNode = { displayName: "", children: [] }; + const { rows, promotedRootSharedUids } = await listVaultRootFolders(storage); + const optsWithPromoted: BuildOpts = { ...opts, promotedRootSharedUids }; + for (const folderRow of rows) { + node.children.push( + await buildFolderSubtree(storage, folderRow.uid, optsWithPromoted), + ); + } + const listed = await listFolder(storage, { + folderUid: undefined, + showFolders: true, + showRecords: opts.showRecords, + }); + if (opts.showRecords && listed.records?.length) { + node.records = listed.records.map((recordRow) => { + const record = storage.getByUid( + VaultObjectKind.Record, + recordRow.uid, + ); + const title = record ? getRecordTitle(record) : recordRow.name; + return { + display: formatTreeRecordName(title, opts.verbose, recordRow.uid), + }; + }); + } + return node; } async function resolveTreeStart( - storage: InMemoryStorage, - session: VaultFolderSession, - folderPath?: string | null + storage: InMemoryStorage, + session: VaultFolderSession, + folderPath?: string | null, ): Promise<{ folderUid: string | null }> { - const trimmedPath = folderPath?.trim() - if (trimmedPath) { - const resolved = await resolveSingleFolder(storage, session, trimmedPath) - return { folderUid: resolved.folderUid } - } - return { folderUid: session.currentFolderUid || null } + const trimmedPath = folderPath?.trim(); + if (trimmedPath) { + const resolved = await resolveSingleFolder(storage, session, trimmedPath); + return { folderUid: resolved.folderUid }; + } + return { folderUid: session.currentFolderUid || null }; } export async function buildFolderTree( - storage: InMemoryStorage, - session: VaultFolderSession, - options: FolderTreeBuildOptions = {} + storage: InMemoryStorage, + session: VaultFolderSession, + options: FolderTreeBuildOptions = {}, ): Promise { - const opts: BuildOpts = { - verbose: options.verbose === true, - showRecords: options.showRecords === true, - showShares: options.showShares === true, - hideSharesKey: options.hideSharesKey === true, - accountUidEmailMap: buildAccountUidEmailMap(storage), - } - - const { folderUid } = await resolveTreeStart(storage, session, options.folderPath) - - const root = - folderUid === null - ? await buildVaultRootTree(storage, opts) - : await buildFolderSubtree(storage, folderUid, opts) - - return { title: options.title || undefined, root } + const opts: BuildOpts = { + verbose: options.verbose === true, + showRecords: options.showRecords === true, + showShares: options.showShares === true, + hideSharesKey: options.hideSharesKey === true, + accountUidEmailMap: buildAccountUidEmailMap(storage), + }; + + const { folderUid } = await resolveTreeStart( + storage, + session, + options.folderPath, + ); + + const root = + folderUid === null + ? await buildVaultRootTree(storage, opts) + : await buildFolderSubtree(storage, folderUid, opts); + + return { title: options.title || undefined, root }; } type TreeItem = - | { kind: TreeItemKind.Permission; display: string } - | { kind: TreeItemKind.Record; display: string } - | { kind: TreeItemKind.Folder; node: FolderTreeNode } + | { kind: TreeItemKind.Permission; display: string } + | { kind: TreeItemKind.Record; display: string } + | { kind: TreeItemKind.Folder; node: FolderTreeNode }; function gatherItems(node: FolderTreeNode): TreeItem[] { - const items: TreeItem[] = [] - if (node.permissions) { - for (const permission of node.permissions) { - items.push({ kind: TreeItemKind.Permission, display: permission.display }) - } - } - for (const child of node.children) { - items.push({ kind: TreeItemKind.Folder, node: child }) + const items: TreeItem[] = []; + if (node.permissions) { + for (const permission of node.permissions) { + items.push({ + kind: TreeItemKind.Permission, + display: permission.display, + }); } - if (node.records) { - for (const record of node.records) { - items.push({ kind: TreeItemKind.Record, display: record.display }) - } + } + for (const child of node.children) { + items.push({ kind: TreeItemKind.Folder, node: child }); + } + if (node.records) { + for (const record of node.records) { + items.push({ kind: TreeItemKind.Record, display: record.display }); } - return items + } + return items; } export function renderFolderTreeAscii(result: FolderTreeResult): string { - const lines: string[] = [] - if (result.title) { - lines.push(result.title) - } - renderNode(result.root, lines, true, '', true) - return lines.join('\n') + const lines: string[] = []; + if (result.title) { + lines.push(result.title); + } + renderNode(result.root, lines, true, "", true); + return lines.join("\n"); } -function renderNode(node: FolderTreeNode, lines: string[], isRoot: boolean, prefix: string, isLast: boolean): void { - if (isRoot) { - if (node.displayName) { - lines.push(node.displayName) - } - } else { - const connector = isLast ? '\\-- ' : '+-- ' - lines.push(prefix + connector + node.displayName) +function renderNode( + node: FolderTreeNode, + lines: string[], + isRoot: boolean, + prefix: string, + isLast: boolean, +): void { + if (isRoot) { + if (node.displayName) { + lines.push(node.displayName); } - - const childBase = isRoot ? ' ' : prefix + (isLast ? ' ' : '| ') - const items = gatherItems(node) - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - const isLastItem = itemIndex === items.length - 1 - const item = items[itemIndex] - if (item.kind === TreeItemKind.Permission || item.kind === TreeItemKind.Record) { - const connector = isLastItem ? '\\-- ' : '+-- ' - lines.push(childBase + connector + item.display) - } else { - renderNode(item.node, lines, false, childBase, isLastItem) - } + } else { + const connector = isLast ? "\\-- " : "+-- "; + lines.push(prefix + connector + node.displayName); + } + + const childBase = isRoot ? " " : prefix + (isLast ? " " : "| "); + const items = gatherItems(node); + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const isLastItem = itemIndex === items.length - 1; + const item = items[itemIndex]; + if ( + item.kind === TreeItemKind.Permission || + item.kind === TreeItemKind.Record + ) { + const connector = isLastItem ? "\\-- " : "+-- "; + lines.push(childBase + connector + item.display); + } else { + renderNode(item.node, lines, false, childBase, isLastItem); } + } } export async function folderTreeAscii( - storage: InMemoryStorage, - session: VaultFolderSession, - options?: FolderTreeBuildOptions + storage: InMemoryStorage, + session: VaultFolderSession, + options?: FolderTreeBuildOptions, ): Promise { - const built = await buildFolderTree(storage, session, options || {}) - return renderFolderTreeAscii(built) + const built = await buildFolderTree(storage, session, options || {}); + return renderFolderTreeAscii(built); } diff --git a/KeeperSdk/src/folders/getFolder.ts b/KeeperSdk/src/folders/getFolder.ts index cad1b1c0..6ea72b91 100644 --- a/KeeperSdk/src/folders/getFolder.ts +++ b/KeeperSdk/src/folders/getFolder.ts @@ -1,234 +1,268 @@ import type { - DSharedFolder, - DSharedFolderFolder, - DSharedFolderRecord, - DSharedFolderUser, - DUserFolder, -} from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError } from '../utils' -import { findParentFolderUid } from './changeDirectory' -import { FolderKind, FolderObjectType, VaultObjectKind, sharedFolderFolderName, userFolderName } from './folderHelpers' + DSharedFolder, + DSharedFolderFolder, + DSharedFolderRecord, + DSharedFolderUser, + DUserFolder, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { KeeperSdkError } from "../utils"; +import { findParentFolderUid } from "./changeDirectory"; +import { + FolderKind, + FolderObjectType, + VaultObjectKind, + sharedFolderFolderName, + userFolderName, +} from "./folderHelpers"; export enum GetFolderFormat { - Detail = 'detail', - JSON = 'json', + Detail = "detail", + JSON = "json", } -export type GetFolderFormatInput = GetFolderFormat | `${GetFolderFormat}` +export type GetFolderFormatInput = GetFolderFormat | `${GetFolderFormat}`; export type GetFolderOptions = { - format?: GetFolderFormatInput -} + format?: GetFolderFormatInput; +}; -export type GetFolderResult = GetFolderResultFolder | GetFolderResultSharedFolder +export type GetFolderResult = + | GetFolderResultFolder + | GetFolderResultSharedFolder; export type GetFolderResultFolder = { - objectType: FolderObjectType.Folder - format: GetFolderFormat - folder_uid: string - folder_type: FolderKind.UserFolder | FolderKind.SharedFolderFolder - name: string - parent_uid: string | null - shared_folder_scope_uid?: string - json?: Record -} + objectType: FolderObjectType.Folder; + format: GetFolderFormat; + folder_uid: string; + folder_type: FolderKind.UserFolder | FolderKind.SharedFolderFolder; + name: string; + parent_uid: string | null; + shared_folder_scope_uid?: string; + json?: Record; +}; export type GetFolderResultSharedFolder = { - objectType: FolderObjectType.SharedFolder - format: GetFolderFormat - shared_folder_uid: string - name: string - default_can_edit: boolean - default_can_share: boolean - default_manage_records: boolean - default_manage_users: boolean - record_permissions?: { - record_uid: string - can_edit: boolean - can_share: boolean - owner: boolean - }[] - user_permissions?: { - account_username?: string - manage_records: boolean - manage_users: boolean - }[] - json?: Record -} + objectType: FolderObjectType.SharedFolder; + format: GetFolderFormat; + shared_folder_uid: string; + name: string; + default_can_edit: boolean; + default_can_share: boolean; + default_manage_records: boolean; + default_manage_users: boolean; + record_permissions?: { + record_uid: string; + can_edit: boolean; + can_share: boolean; + owner: boolean; + }[]; + user_permissions?: { + account_username?: string; + manage_records: boolean; + manage_users: boolean; + }[]; + json?: Record; +}; export type FoundFolder = - | { kind: FolderKind.UserFolder; folder: DUserFolder } - | { kind: FolderKind.SharedFolder; folder: DSharedFolder } - | { kind: FolderKind.SharedFolderFolder; folder: DSharedFolderFolder } + | { kind: FolderKind.UserFolder; folder: DUserFolder } + | { kind: FolderKind.SharedFolder; folder: DSharedFolder } + | { kind: FolderKind.SharedFolderFolder; folder: DSharedFolderFolder }; function findByUidOrName( - items: Iterable, - nameOrUid: string, - getUid: (item: T) => string, - getName: (item: T) => string + items: Iterable, + nameOrUid: string, + getUid: (item: T) => string, + getName: (item: T) => string, ): T | undefined { - const trimmedNameOrUid = nameOrUid.trim() - if (!trimmedNameOrUid) return undefined - const lowerNameOrUid = trimmedNameOrUid.toLowerCase() - let exactNameHit: T | undefined - let lowerNameHit: T | undefined - for (const item of items) { - if (getUid(item) === trimmedNameOrUid) return item - if (!exactNameHit) { - const itemName = getName(item) - if (itemName === trimmedNameOrUid) { - exactNameHit = item - } else if (!lowerNameHit && itemName.toLowerCase() === lowerNameOrUid) { - lowerNameHit = item - } - } + const trimmedNameOrUid = nameOrUid.trim(); + if (!trimmedNameOrUid) return undefined; + const lowerNameOrUid = trimmedNameOrUid.toLowerCase(); + let exactNameHit: T | undefined; + let lowerNameHit: T | undefined; + for (const item of items) { + if (getUid(item) === trimmedNameOrUid) return item; + if (!exactNameHit) { + const itemName = getName(item); + if (itemName === trimmedNameOrUid) { + exactNameHit = item; + } else if (!lowerNameHit && itemName.toLowerCase() === lowerNameOrUid) { + lowerNameHit = item; + } } - return exactNameHit || lowerNameHit + } + return exactNameHit || lowerNameHit; } -function findSharedFolder(storage: InMemoryStorage, nameOrUid: string): DSharedFolder | undefined { - return findByUidOrName( - storage.getAll(FolderKind.SharedFolder), - nameOrUid, - (sharedFolder) => sharedFolder.uid, - (sharedFolder) => (sharedFolder.name || '').trim() - ) +function findSharedFolder( + storage: InMemoryStorage, + nameOrUid: string, +): DSharedFolder | undefined { + return findByUidOrName( + storage.getAll(FolderKind.SharedFolder), + nameOrUid, + (sharedFolder) => sharedFolder.uid, + (sharedFolder) => (sharedFolder.name || "").trim(), + ); } -function findUserFolder(storage: InMemoryStorage, nameOrUid: string): DUserFolder | undefined { - return findByUidOrName( - storage.getAll(FolderKind.UserFolder), - nameOrUid, - (userFolder) => userFolder.uid, - (userFolder) => userFolderName(userFolder) - ) +function findUserFolder( + storage: InMemoryStorage, + nameOrUid: string, +): DUserFolder | undefined { + return findByUidOrName( + storage.getAll(FolderKind.UserFolder), + nameOrUid, + (userFolder) => userFolder.uid, + (userFolder) => userFolderName(userFolder), + ); } -function findSharedFolderFolder(storage: InMemoryStorage, nameOrUid: string): DSharedFolderFolder | undefined { - return findByUidOrName( - storage.getAll(FolderKind.SharedFolderFolder), - nameOrUid, - (sharedFolderFolder) => sharedFolderFolder.uid, - (sharedFolderFolder) => sharedFolderFolderName(sharedFolderFolder) - ) +function findSharedFolderFolder( + storage: InMemoryStorage, + nameOrUid: string, +): DSharedFolderFolder | undefined { + return findByUidOrName( + storage.getAll(FolderKind.SharedFolderFolder), + nameOrUid, + (sharedFolderFolder) => sharedFolderFolder.uid, + (sharedFolderFolder) => sharedFolderFolderName(sharedFolderFolder), + ); } -export function findFolder(storage: InMemoryStorage, nameOrUid: string): FoundFolder | undefined { - const trimmedNameOrUid = nameOrUid.trim() - if (!trimmedNameOrUid) return undefined +export function findFolder( + storage: InMemoryStorage, + nameOrUid: string, +): FoundFolder | undefined { + const trimmedNameOrUid = nameOrUid.trim(); + if (!trimmedNameOrUid) return undefined; - const sharedFolder = findSharedFolder(storage, trimmedNameOrUid) - if (sharedFolder) return { kind: FolderKind.SharedFolder, folder: sharedFolder } + const sharedFolder = findSharedFolder(storage, trimmedNameOrUid); + if (sharedFolder) + return { kind: FolderKind.SharedFolder, folder: sharedFolder }; - const userFolder = findUserFolder(storage, trimmedNameOrUid) - if (userFolder) return { kind: FolderKind.UserFolder, folder: userFolder } + const userFolder = findUserFolder(storage, trimmedNameOrUid); + if (userFolder) return { kind: FolderKind.UserFolder, folder: userFolder }; - const sharedFolderFolder = findSharedFolderFolder(storage, trimmedNameOrUid) - if (sharedFolderFolder) return { kind: FolderKind.SharedFolderFolder, folder: sharedFolderFolder } + const sharedFolderFolder = findSharedFolderFolder(storage, trimmedNameOrUid); + if (sharedFolderFolder) + return { kind: FolderKind.SharedFolderFolder, folder: sharedFolderFolder }; - return undefined + return undefined; } async function formatRegularFolder( - storage: InMemoryStorage, - folder: DUserFolder | DSharedFolderFolder, - format: GetFolderFormat + storage: InMemoryStorage, + folder: DUserFolder | DSharedFolderFolder, + format: GetFolderFormat, ): Promise { - const isUser = folder.kind === FolderKind.UserFolder - const folder_uid = folder.uid - const folder_type: GetFolderResultFolder['folder_type'] = isUser - ? FolderKind.UserFolder - : FolderKind.SharedFolderFolder - const name = isUser ? userFolderName(folder) : sharedFolderFolderName(folder) - const parent_uid = await findParentFolderUid(storage, folder_uid) - - const base: GetFolderResultFolder = { - objectType: FolderObjectType.Folder, - format, - folder_uid, - folder_type, - name, - parent_uid, - } - if (!isUser) { - base.shared_folder_scope_uid = (folder as DSharedFolderFolder).sharedFolderUid - } - if (format === GetFolderFormat.JSON) { - base.json = { ...base, objectType: FolderObjectType.Folder } - } - return base + const isUser = folder.kind === FolderKind.UserFolder; + const folder_uid = folder.uid; + const folder_type: GetFolderResultFolder["folder_type"] = isUser + ? FolderKind.UserFolder + : FolderKind.SharedFolderFolder; + const name = isUser ? userFolderName(folder) : sharedFolderFolderName(folder); + const parent_uid = await findParentFolderUid(storage, folder_uid); + + const base: GetFolderResultFolder = { + objectType: FolderObjectType.Folder, + format, + folder_uid, + folder_type, + name, + parent_uid, + }; + if (!isUser) { + base.shared_folder_scope_uid = ( + folder as DSharedFolderFolder + ).sharedFolderUid; + } + if (format === GetFolderFormat.JSON) { + base.json = { ...base, objectType: FolderObjectType.Folder }; + } + return base; } function formatSharedFolder( - storage: InMemoryStorage, - sharedFolder: DSharedFolder, - format: GetFolderFormat + storage: InMemoryStorage, + sharedFolder: DSharedFolder, + format: GetFolderFormat, ): GetFolderResultSharedFolder { - const sharedFolderUid = sharedFolder.uid - const recordPermissions = storage - .getAll(VaultObjectKind.SharedFolderRecord) - .filter((record) => record.sharedFolderUid === sharedFolderUid) - .map((record) => ({ - record_uid: record.recordUid, - can_edit: record.canEdit, - can_share: record.canShare, - owner: record.owner, - })) - const userPermissions = storage - .getAll(VaultObjectKind.SharedFolderUser) - .filter((user) => user.sharedFolderUid === sharedFolderUid) - .map((user) => ({ - account_username: user.accountUsername, - manage_records: user.manageRecords, - manage_users: user.manageUsers, - })) - - const base: GetFolderResultSharedFolder = { - objectType: FolderObjectType.SharedFolder, - format, - shared_folder_uid: sharedFolder.uid, - name: (sharedFolder.name || '').trim() || sharedFolder.uid, - default_can_edit: sharedFolder.defaultCanEdit, - default_can_share: sharedFolder.defaultCanShare, - default_manage_records: sharedFolder.defaultManageRecords, - default_manage_users: sharedFolder.defaultManageUsers, - record_permissions: recordPermissions.length ? recordPermissions : undefined, - user_permissions: userPermissions.length ? userPermissions : undefined, - } - if (format === GetFolderFormat.JSON) { - base.json = { ...base, objectType: FolderObjectType.SharedFolder } - } - return base + const sharedFolderUid = sharedFolder.uid; + const recordPermissions = storage + .getAll(VaultObjectKind.SharedFolderRecord) + .filter((record) => record.sharedFolderUid === sharedFolderUid) + .map((record) => ({ + record_uid: record.recordUid, + can_edit: record.canEdit, + can_share: record.canShare, + owner: record.owner, + })); + const userPermissions = storage + .getAll(VaultObjectKind.SharedFolderUser) + .filter((user) => user.sharedFolderUid === sharedFolderUid) + .map((user) => ({ + account_username: user.accountUsername, + manage_records: user.manageRecords, + manage_users: user.manageUsers, + })); + + const base: GetFolderResultSharedFolder = { + objectType: FolderObjectType.SharedFolder, + format, + shared_folder_uid: sharedFolder.uid, + name: (sharedFolder.name || "").trim() || sharedFolder.uid, + default_can_edit: sharedFolder.defaultCanEdit, + default_can_share: sharedFolder.defaultCanShare, + default_manage_records: sharedFolder.defaultManageRecords, + default_manage_users: sharedFolder.defaultManageUsers, + record_permissions: recordPermissions.length + ? recordPermissions + : undefined, + user_permissions: userPermissions.length ? userPermissions : undefined, + }; + if (format === GetFolderFormat.JSON) { + base.json = { ...base, objectType: FolderObjectType.SharedFolder }; + } + return base; } -function normalizeFormat(format: GetFolderFormatInput | undefined): GetFolderFormat { - if (format === GetFolderFormat.JSON) return GetFolderFormat.JSON - return GetFolderFormat.Detail +function normalizeFormat( + format: GetFolderFormatInput | undefined, +): GetFolderFormat { + if (format === GetFolderFormat.JSON) return GetFolderFormat.JSON; + return GetFolderFormat.Detail; } export async function getFolder( - storage: InMemoryStorage, - uidOrName: string, - options: GetFolderOptions = {} + storage: InMemoryStorage, + uidOrName: string, + options: GetFolderOptions = {}, ): Promise { - const trimmed = uidOrName.trim() - if (!trimmed) { - throw new KeeperSdkError('Folder UID or name is required.', 'missing_folder_ref') - } + const trimmed = uidOrName.trim(); + if (!trimmed) { + throw new KeeperSdkError( + "Folder UID or name is required.", + "missing_folder_ref", + ); + } - const found = findFolder(storage, trimmed) - if (!found) { - throw new KeeperSdkError(`"${trimmed}" not found as a folder or shared folder`, 'folder_not_found') - } + const found = findFolder(storage, trimmed); + if (!found) { + throw new KeeperSdkError( + `"${trimmed}" not found as a folder or shared folder`, + "folder_not_found", + ); + } - const format: GetFolderFormat = normalizeFormat(options.format) + const format: GetFolderFormat = normalizeFormat(options.format); - switch (found.kind) { - case FolderKind.SharedFolder: - return formatSharedFolder(storage, found.folder, format) - case FolderKind.UserFolder: - case FolderKind.SharedFolderFolder: - return formatRegularFolder(storage, found.folder, format) - } + switch (found.kind) { + case FolderKind.SharedFolder: + return formatSharedFolder(storage, found.folder, format); + case FolderKind.UserFolder: + case FolderKind.SharedFolderFolder: + return formatRegularFolder(storage, found.folder, format); + } } diff --git a/KeeperSdk/src/folders/listFolder.ts b/KeeperSdk/src/folders/listFolder.ts index 619dd4f9..d4334509 100644 --- a/KeeperSdk/src/folders/listFolder.ts +++ b/KeeperSdk/src/folders/listFolder.ts @@ -1,358 +1,454 @@ import type { - DRecord, - DRecordMetadata, - DSharedFolder, - DSharedFolderFolder, - DUserFolder, - VaultStorageData, - VaultStorageKind, -} from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError } from '../utils' -import { getRecordTitle, getRecordType } from '../records/RecordUtils' + DRecord, + DRecordMetadata, + DSharedFolder, + DSharedFolderFolder, + DUserFolder, + VaultStorageData, + VaultStorageKind, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { KeeperSdkError } from "../utils"; +import { getRecordTitle, getRecordType } from "../records/RecordUtils"; import { - FolderKind, - VaultObjectKind, - getUserFolderParentMap, - globToRegex, - sharedFolderFolderName, - sharedFolderName, - userFolderName, -} from './folderHelpers' + FolderKind, + VaultObjectKind, + getUserFolderParentMap, + globToRegex, + sharedFolderFolderName, + sharedFolderName, + userFolderName, +} from "./folderHelpers"; export type ListFolderOptions = { - folderUid?: string | null - pattern?: string | null - showFolders?: boolean - showRecords?: boolean - detail?: boolean -} + folderUid?: string | null; + pattern?: string | null; + showFolders?: boolean; + showRecords?: boolean; + detail?: boolean; +}; export type ListFolderFolderSimple = { - uid: string - name: string - folderKind: FolderKind -} + uid: string; + name: string; + folderKind: FolderKind; +}; export type ListFolderRecordSimple = { - uid: string - name: string - type: string -} + uid: string; + name: string; + type: string; +}; export type ListFolderFolderDetail = ListFolderFolderSimple & { - flags: string - recordCount: number - subfolderCount: number -} + flags: string; + recordCount: number; + subfolderCount: number; +}; export type ListFolderRecordDetail = ListFolderRecordSimple & { - flags: string - version: number - isOwner: boolean - hasAttachments: boolean - isShared: boolean -} + flags: string; + version: number; + isOwner: boolean; + hasAttachments: boolean; + isShared: boolean; +}; export type ListFolderResult = - | { - detail: true - folders: ListFolderFolderDetail[] - records: ListFolderRecordDetail[] - } - | { - detail: false - folders: ListFolderFolderSimple[] - records: ListFolderRecordSimple[] - } + | { + detail: true; + folders: ListFolderFolderDetail[]; + records: ListFolderRecordDetail[]; + } + | { + detail: false; + folders: ListFolderFolderSimple[]; + records: ListFolderRecordSimple[]; + }; function recordHasAttachments(record: DRecord): boolean { - const fileIds = record.udata?.file_ids - if (Array.isArray(fileIds) && fileIds.length > 0) return true - const files = (record.extra as { files?: unknown[] } | undefined)?.files - return Array.isArray(files) && files.length > 0 + const fileIds = record.udata?.file_ids; + if (Array.isArray(fileIds) && fileIds.length > 0) return true; + const files = (record.extra as { files?: unknown[] } | undefined)?.files; + return Array.isArray(files) && files.length > 0; } -function buildFolderFlags(folderKind: ListFolderFolderSimple['folderKind']): string { - const isShared = folderKind !== FolderKind.UserFolder - return `f--${isShared ? 'S' : '-'}` +function buildFolderFlags( + folderKind: ListFolderFolderSimple["folderKind"], +): string { + const isShared = folderKind !== FolderKind.UserFolder; + return `f--${isShared ? "S" : "-"}`; } -function buildRecordFlags(record: DRecord, metadata: DRecordMetadata | undefined): string { - const isOwner = metadata?.owner === true - const hasAttachments = recordHasAttachments(record) - const isShared = !!record.shared - return `r${isOwner ? 'O' : '-'}${hasAttachments ? 'A' : '-'}${isShared ? 'S' : '-'}` +function buildRecordFlags( + record: DRecord, + metadata: DRecordMetadata | undefined, +): string { + const isOwner = metadata?.owner === true; + const hasAttachments = recordHasAttachments(record); + const isShared = !!record.shared; + return `r${isOwner ? "O" : "-"}${hasAttachments ? "A" : "-"}${isShared ? "S" : "-"}`; } -export async function listRootUserFolders(storage: InMemoryStorage): Promise { - const userFolders = storage.getAll(FolderKind.UserFolder) - const childToParent = await getUserFolderParentMap(storage) - return userFolders.filter((userFolder) => !childToParent.has(userFolder.uid)) +export async function listRootUserFolders( + storage: InMemoryStorage, +): Promise { + const userFolders = storage.getAll(FolderKind.UserFolder); + const childToParent = await getUserFolderParentMap(storage); + return userFolders.filter((userFolder) => !childToParent.has(userFolder.uid)); } async function buildFolderParentMap( - storage: InMemoryStorage + storage: InMemoryStorage, ): Promise> { - const childToParent = new Map() - const parentKinds: VaultStorageKind[] = [ - FolderKind.UserFolder, - FolderKind.SharedFolder, - FolderKind.SharedFolderFolder, - ] - for (const kind of parentKinds) { - for (const candidate of storage.getAll<{ uid: string } & VaultStorageData>(kind)) { - const candidateUid = (candidate as { uid: string }).uid - if (!candidateUid) continue - const dependencies = (await storage.getDependencies(candidateUid)) || [] - for (const dependency of dependencies) { - if ( - dependency.kind === FolderKind.UserFolder || - dependency.kind === FolderKind.SharedFolder || - dependency.kind === FolderKind.SharedFolderFolder - ) { - if (!childToParent.has(dependency.uid)) { - childToParent.set(dependency.uid, { uid: candidateUid, kind }) - } - } - } + const childToParent = new Map< + string, + { uid: string; kind: VaultStorageKind } + >(); + const parentKinds: VaultStorageKind[] = [ + FolderKind.UserFolder, + FolderKind.SharedFolder, + FolderKind.SharedFolderFolder, + ]; + for (const kind of parentKinds) { + for (const candidate of storage.getAll<{ uid: string } & VaultStorageData>( + kind, + )) { + const candidateUid = (candidate as { uid: string }).uid; + if (!candidateUid) continue; + const dependencies = (await storage.getDependencies(candidateUid)) || []; + for (const dependency of dependencies) { + if ( + dependency.kind === FolderKind.UserFolder || + dependency.kind === FolderKind.SharedFolder || + dependency.kind === FolderKind.SharedFolderFolder + ) { + if (!childToParent.has(dependency.uid)) { + childToParent.set(dependency.uid, { uid: candidateUid, kind }); + } } + } } - return childToParent + } + return childToParent; } export async function listVaultRootFolders(storage: InMemoryStorage): Promise<{ - rows: ListFolderFolderSimple[] - promotedRootSharedUids: Set + rows: ListFolderFolderSimple[]; + promotedRootSharedUids: Set; }> { - const rows: ListFolderFolderSimple[] = [] - const seen = new Set() - const promotedRootSharedUids = new Set() - - for (const userFolder of await listRootUserFolders(storage)) { - if (seen.has(userFolder.uid)) continue - seen.add(userFolder.uid) - rows.push({ - uid: userFolder.uid, - name: userFolderName(userFolder), - folderKind: FolderKind.UserFolder, - }) - } - - const rootDependencies = (await storage.getDependencies('')) || [] - for (const dependency of rootDependencies) { - if (dependency.kind === FolderKind.SharedFolder) { - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, dependency.uid) - if (!sharedFolder || seen.has(sharedFolder.uid)) continue - seen.add(sharedFolder.uid) - rows.push({ - uid: sharedFolder.uid, - name: sharedFolderName(sharedFolder), - folderKind: FolderKind.SharedFolder, - }) - } else if (dependency.kind === FolderKind.SharedFolderFolder) { - const sharedFolderFolder = storage.getByUid( - FolderKind.SharedFolderFolder, - dependency.uid - ) - if (!sharedFolderFolder || seen.has(sharedFolderFolder.uid)) continue - seen.add(sharedFolderFolder.uid) - rows.push({ - uid: sharedFolderFolder.uid, - name: sharedFolderFolderName(sharedFolderFolder), - folderKind: FolderKind.SharedFolderFolder, - }) - } + const rows: ListFolderFolderSimple[] = []; + const seen = new Set(); + const promotedRootSharedUids = new Set(); + + for (const userFolder of await listRootUserFolders(storage)) { + if (seen.has(userFolder.uid)) continue; + seen.add(userFolder.uid); + rows.push({ + uid: userFolder.uid, + name: userFolderName(userFolder), + folderKind: FolderKind.UserFolder, + }); + } + + const rootDependencies = (await storage.getDependencies("")) || []; + for (const dependency of rootDependencies) { + if (dependency.kind === FolderKind.SharedFolder) { + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + dependency.uid, + ); + if (!sharedFolder || seen.has(sharedFolder.uid)) continue; + seen.add(sharedFolder.uid); + rows.push({ + uid: sharedFolder.uid, + name: sharedFolderName(sharedFolder), + folderKind: FolderKind.SharedFolder, + }); + } else if (dependency.kind === FolderKind.SharedFolderFolder) { + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + dependency.uid, + ); + if (!sharedFolderFolder || seen.has(sharedFolderFolder.uid)) continue; + seen.add(sharedFolderFolder.uid); + rows.push({ + uid: sharedFolderFolder.uid, + name: sharedFolderFolderName(sharedFolderFolder), + folderKind: FolderKind.SharedFolderFolder, + }); } - - const parentMap = await buildFolderParentMap(storage) - for (const sharedFolder of storage.getAll(FolderKind.SharedFolder)) { - if (seen.has(sharedFolder.uid)) continue - const parent = parentMap.get(sharedFolder.uid) - if (parent && (parent.kind === FolderKind.SharedFolder || parent.kind === FolderKind.SharedFolderFolder)) { - continue - } - seen.add(sharedFolder.uid) - promotedRootSharedUids.add(sharedFolder.uid) - rows.push({ - uid: sharedFolder.uid, - name: sharedFolderName(sharedFolder), - folderKind: FolderKind.SharedFolder, - }) + } + + const parentMap = await buildFolderParentMap(storage); + for (const sharedFolder of storage.getAll( + FolderKind.SharedFolder, + )) { + if (seen.has(sharedFolder.uid)) continue; + const parent = parentMap.get(sharedFolder.uid); + if ( + parent && + (parent.kind === FolderKind.SharedFolder || + parent.kind === FolderKind.SharedFolderFolder) + ) { + continue; } - - rows.sort((rowA, rowB) => rowA.name.localeCompare(rowB.name, undefined, { sensitivity: 'base' })) - - return { rows, promotedRootSharedUids } + seen.add(sharedFolder.uid); + promotedRootSharedUids.add(sharedFolder.uid); + rows.push({ + uid: sharedFolder.uid, + name: sharedFolderName(sharedFolder), + folderKind: FolderKind.SharedFolder, + }); + } + + rows.sort((rowA, rowB) => + rowA.name.localeCompare(rowB.name, undefined, { sensitivity: "base" }), + ); + + return { rows, promotedRootSharedUids }; } -function resolveFolderContainer(storage: InMemoryStorage, folderUid: string): { kind: VaultStorageKind; uid: string } { - if (storage.getByUid(FolderKind.UserFolder, folderUid)) { - return { kind: FolderKind.UserFolder, uid: folderUid } - } - if (storage.getByUid(FolderKind.SharedFolder, folderUid)) { - return { kind: FolderKind.SharedFolder, uid: folderUid } - } - if (storage.getByUid(FolderKind.SharedFolderFolder, folderUid)) { - return { kind: FolderKind.SharedFolderFolder, uid: folderUid } - } - throw new KeeperSdkError(`Folder "${folderUid}" not found`, 'folder_not_found') +function resolveFolderContainer( + storage: InMemoryStorage, + folderUid: string, +): { kind: VaultStorageKind; uid: string } { + if (storage.getByUid(FolderKind.UserFolder, folderUid)) { + return { kind: FolderKind.UserFolder, uid: folderUid }; + } + if (storage.getByUid(FolderKind.SharedFolder, folderUid)) { + return { kind: FolderKind.SharedFolder, uid: folderUid }; + } + if ( + storage.getByUid( + FolderKind.SharedFolderFolder, + folderUid, + ) + ) { + return { kind: FolderKind.SharedFolderFolder, uid: folderUid }; + } + throw new KeeperSdkError( + `Folder "${folderUid}" not found`, + "folder_not_found", + ); } -function getRecordMetadata(storage: InMemoryStorage, recordUid: string): DRecordMetadata | undefined { - return storage.getByUid(VaultObjectKind.Metadata, recordUid) +function getRecordMetadata( + storage: InMemoryStorage, + recordUid: string, +): DRecordMetadata | undefined { + return storage.getByUid(VaultObjectKind.Metadata, recordUid); } -export function findFolderUidByNameOrUid(storage: InMemoryStorage, nameOrUid: string): string | undefined { - const trimmedNameOrUid = nameOrUid.trim() - if (!trimmedNameOrUid) return undefined - +export function findFolderUidByNameOrUid( + storage: InMemoryStorage, + nameOrUid: string, +): string | undefined { + const trimmedNameOrUid = nameOrUid.trim(); + if (!trimmedNameOrUid) return undefined; + + if ( + storage.getByUid(FolderKind.UserFolder, trimmedNameOrUid) || + storage.getByUid( + FolderKind.SharedFolder, + trimmedNameOrUid, + ) || + storage.getByUid( + FolderKind.SharedFolderFolder, + trimmedNameOrUid, + ) + ) { + return trimmedNameOrUid; + } + + const lowerNameOrUid = trimmedNameOrUid.toLowerCase(); + for (const userFolder of storage.getAll(FolderKind.UserFolder)) { + if (userFolderName(userFolder).toLowerCase() === lowerNameOrUid) + return userFolder.uid; + } + for (const sharedFolder of storage.getAll( + FolderKind.SharedFolder, + )) { + if (sharedFolderName(sharedFolder).toLowerCase() === lowerNameOrUid) + return sharedFolder.uid; + } + for (const sharedFolderFolder of storage.getAll( + FolderKind.SharedFolderFolder, + )) { if ( - storage.getByUid(FolderKind.UserFolder, trimmedNameOrUid) || - storage.getByUid(FolderKind.SharedFolder, trimmedNameOrUid) || - storage.getByUid(FolderKind.SharedFolderFolder, trimmedNameOrUid) - ) { - return trimmedNameOrUid - } - - const lowerNameOrUid = trimmedNameOrUid.toLowerCase() - for (const userFolder of storage.getAll(FolderKind.UserFolder)) { - if (userFolderName(userFolder).toLowerCase() === lowerNameOrUid) return userFolder.uid - } - for (const sharedFolder of storage.getAll(FolderKind.SharedFolder)) { - if (sharedFolderName(sharedFolder).toLowerCase() === lowerNameOrUid) return sharedFolder.uid - } - for (const sharedFolderFolder of storage.getAll(FolderKind.SharedFolderFolder)) { - if (sharedFolderFolderName(sharedFolderFolder).toLowerCase() === lowerNameOrUid) return sharedFolderFolder.uid - } - return undefined + sharedFolderFolderName(sharedFolderFolder).toLowerCase() === + lowerNameOrUid + ) + return sharedFolderFolder.uid; + } + return undefined; } async function countFolderChildren( - storage: InMemoryStorage, - uid: string + storage: InMemoryStorage, + uid: string, ): Promise<{ records: number; subfolders: number }> { - const dependencies = (await storage.getDependencies(uid)) || [] - let records = 0 - let subfolders = 0 - for (const dependency of dependencies) { - if (dependency.kind === VaultObjectKind.Record) records++ - else if ( - dependency.kind === FolderKind.UserFolder || - dependency.kind === FolderKind.SharedFolder || - dependency.kind === FolderKind.SharedFolderFolder - ) { - subfolders++ - } + const dependencies = (await storage.getDependencies(uid)) || []; + let records = 0; + let subfolders = 0; + for (const dependency of dependencies) { + if (dependency.kind === VaultObjectKind.Record) records++; + else if ( + dependency.kind === FolderKind.UserFolder || + dependency.kind === FolderKind.SharedFolder || + dependency.kind === FolderKind.SharedFolderFolder + ) { + subfolders++; } - return { records, subfolders } + } + return { records, subfolders }; } -export async function listFolder(storage: InMemoryStorage, options: ListFolderOptions = {}): Promise { - const showFolders = options.showFolders !== false - const showRecords = options.showRecords !== false - const detail = options.detail === true - const patternRaw = options.pattern?.trim() || null - const regex = patternRaw ? globToRegex(patternRaw) : null - - const folderUidOpt = options.folderUid - let parentKey: string | null = null - - if (folderUidOpt !== undefined && folderUidOpt !== null && folderUidOpt !== '') { - parentKey = resolveFolderContainer(storage, folderUidOpt).uid - } - - const dependencies = - parentKey === null - ? (await storage.getDependencies('')) || [] - : (await storage.getDependencies(parentKey)) || [] - - const folderRows: ListFolderFolderSimple[] = [] - const recordRows: ListFolderRecordSimple[] = [] - - const matches = (name: string, uid: string): boolean => { - if (!regex) return true - return regex.test(name) || regex.test(uid) - } - - if (showFolders && parentKey === null) { - const { rows } = await listVaultRootFolders(storage) - for (const row of rows) { - if (!matches(row.name, row.uid)) continue - folderRows.push(row) - } - } - - for (const dependency of dependencies) { - if (dependency.kind === FolderKind.UserFolder && showFolders && parentKey !== null) { - const userFolder = storage.getByUid(FolderKind.UserFolder, dependency.uid) - if (!userFolder) continue - const name = userFolderName(userFolder) - if (!matches(name, userFolder.uid)) continue - folderRows.push({ uid: userFolder.uid, name, folderKind: FolderKind.UserFolder }) - } else if (dependency.kind === FolderKind.SharedFolder && showFolders && parentKey !== null) { - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, dependency.uid) - if (!sharedFolder) continue - const name = sharedFolderName(sharedFolder) - if (!matches(name, sharedFolder.uid)) continue - folderRows.push({ uid: sharedFolder.uid, name, folderKind: FolderKind.SharedFolder }) - } else if (dependency.kind === FolderKind.SharedFolderFolder && showFolders && parentKey !== null) { - const sharedFolderFolder = storage.getByUid( - FolderKind.SharedFolderFolder, - dependency.uid - ) - if (!sharedFolderFolder) continue - const name = sharedFolderFolderName(sharedFolderFolder) - if (!matches(name, sharedFolderFolder.uid)) continue - folderRows.push({ - uid: sharedFolderFolder.uid, - name, - folderKind: FolderKind.SharedFolderFolder, - }) - } else if (dependency.kind === VaultObjectKind.Record && showRecords) { - const record = storage.getByUid(VaultObjectKind.Record, dependency.uid) - if (!record || (record.version !== 2 && record.version !== 3)) continue - const title = getRecordTitle(record) - if (!matches(title, record.uid)) continue - recordRows.push({ - uid: record.uid, - name: title, - type: getRecordType(record), - }) - } +export async function listFolder( + storage: InMemoryStorage, + options: ListFolderOptions = {}, +): Promise { + const showFolders = options.showFolders !== false; + const showRecords = options.showRecords !== false; + const detail = options.detail === true; + const patternRaw = options.pattern?.trim() || null; + const regex = patternRaw ? globToRegex(patternRaw) : null; + + const folderUidOpt = options.folderUid; + let parentKey: string | null = null; + + if ( + folderUidOpt !== undefined && + folderUidOpt !== null && + folderUidOpt !== "" + ) { + parentKey = resolveFolderContainer(storage, folderUidOpt).uid; + } + + const dependencies = + parentKey === null + ? (await storage.getDependencies("")) || [] + : (await storage.getDependencies(parentKey)) || []; + + const folderRows: ListFolderFolderSimple[] = []; + const recordRows: ListFolderRecordSimple[] = []; + + const matches = (name: string, uid: string): boolean => { + if (!regex) return true; + return regex.test(name) || regex.test(uid); + }; + + if (showFolders && parentKey === null) { + const { rows } = await listVaultRootFolders(storage); + for (const row of rows) { + if (!matches(row.name, row.uid)) continue; + folderRows.push(row); } + } - folderRows.sort((rowA, rowB) => rowA.name.localeCompare(rowB.name, undefined, { sensitivity: 'base' })) - recordRows.sort((rowA, rowB) => rowA.name.localeCompare(rowB.name, undefined, { sensitivity: 'base' })) - - if (!detail) { - return { detail: false, folders: folderRows, records: recordRows } + for (const dependency of dependencies) { + if ( + dependency.kind === FolderKind.UserFolder && + showFolders && + parentKey !== null + ) { + const userFolder = storage.getByUid( + FolderKind.UserFolder, + dependency.uid, + ); + if (!userFolder) continue; + const name = userFolderName(userFolder); + if (!matches(name, userFolder.uid)) continue; + folderRows.push({ + uid: userFolder.uid, + name, + folderKind: FolderKind.UserFolder, + }); + } else if ( + dependency.kind === FolderKind.SharedFolder && + showFolders && + parentKey !== null + ) { + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + dependency.uid, + ); + if (!sharedFolder) continue; + const name = sharedFolderName(sharedFolder); + if (!matches(name, sharedFolder.uid)) continue; + folderRows.push({ + uid: sharedFolder.uid, + name, + folderKind: FolderKind.SharedFolder, + }); + } else if ( + dependency.kind === FolderKind.SharedFolderFolder && + showFolders && + parentKey !== null + ) { + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + dependency.uid, + ); + if (!sharedFolderFolder) continue; + const name = sharedFolderFolderName(sharedFolderFolder); + if (!matches(name, sharedFolderFolder.uid)) continue; + folderRows.push({ + uid: sharedFolderFolder.uid, + name, + folderKind: FolderKind.SharedFolderFolder, + }); + } else if (dependency.kind === VaultObjectKind.Record && showRecords) { + const record = storage.getByUid( + VaultObjectKind.Record, + dependency.uid, + ); + if (!record || (record.version !== 2 && record.version !== 3)) continue; + const title = getRecordTitle(record); + if (!matches(title, record.uid)) continue; + recordRows.push({ + uid: record.uid, + name: title, + type: getRecordType(record), + }); } - - const folderDetails: ListFolderFolderDetail[] = await Promise.all( - folderRows.map(async (row) => { - const counts = await countFolderChildren(storage, row.uid) - return { - ...row, - flags: buildFolderFlags(row.folderKind), - recordCount: counts.records, - subfolderCount: counts.subfolders, - } - }) - ) - - const recordDetails: ListFolderRecordDetail[] = recordRows.map((row) => { - const record = storage.getByUid(VaultObjectKind.Record, row.uid)! - const metadata = getRecordMetadata(storage, row.uid) - return { - ...row, - flags: buildRecordFlags(record, metadata), - version: record.version, - isOwner: metadata?.owner === true, - hasAttachments: recordHasAttachments(record), - isShared: !!record.shared, - } - }) - - return { detail: true, folders: folderDetails, records: recordDetails } + } + + folderRows.sort((rowA, rowB) => + rowA.name.localeCompare(rowB.name, undefined, { sensitivity: "base" }), + ); + recordRows.sort((rowA, rowB) => + rowA.name.localeCompare(rowB.name, undefined, { sensitivity: "base" }), + ); + + if (!detail) { + return { detail: false, folders: folderRows, records: recordRows }; + } + + const folderDetails: ListFolderFolderDetail[] = await Promise.all( + folderRows.map(async (row) => { + const counts = await countFolderChildren(storage, row.uid); + return { + ...row, + flags: buildFolderFlags(row.folderKind), + recordCount: counts.records, + subfolderCount: counts.subfolders, + }; + }), + ); + + const recordDetails: ListFolderRecordDetail[] = recordRows.map((row) => { + const record = storage.getByUid(VaultObjectKind.Record, row.uid)!; + const metadata = getRecordMetadata(storage, row.uid); + return { + ...row, + flags: buildRecordFlags(record, metadata), + version: record.version, + isOwner: metadata?.owner === true, + hasAttachments: recordHasAttachments(record), + isShared: !!record.shared, + }; + }); + + return { detail: true, folders: folderDetails, records: recordDetails }; } diff --git a/KeeperSdk/src/folders/updateFolder.ts b/KeeperSdk/src/folders/updateFolder.ts index d444c56f..32f1a512 100644 --- a/KeeperSdk/src/folders/updateFolder.ts +++ b/KeeperSdk/src/folders/updateFolder.ts @@ -1,211 +1,280 @@ -import type { Auth, FolderUpdateRequest } from '@keeper-security/keeperapi' +import type { Auth, FolderUpdateRequest } from "@keeper-security/keeperapi"; import { - encryptForStorage, - encryptObjectForStorage, - folderUpdateCommand, - platform, -} from '@keeper-security/keeperapi' -import type { DSharedFolder, DSharedFolderFolder, DUserFolder } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { anyIsBoolean, isBoolean, isObject, KeeperSdkError, extractErrorMessage } from '../utils' -import { resolveSingleFolder, type VaultFolderSession } from './changeDirectory' -import { FolderKind, FolderResultStatus, validateFolderName } from './folderHelpers' + encryptForStorage, + encryptObjectForStorage, + folderUpdateCommand, + platform, +} from "@keeper-security/keeperapi"; +import type { + DSharedFolder, + DSharedFolderFolder, + DUserFolder, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { + anyIsBoolean, + isBoolean, + isObject, + KeeperSdkError, + extractErrorMessage, +} from "../utils"; +import { + resolveSingleFolder, + type VaultFolderSession, +} from "./changeDirectory"; +import { + FolderKind, + FolderResultStatus, + validateFolderName, +} from "./folderHelpers"; export type UpdateFolderInput = { - folderUid: string - folderName?: string | null - manageUsers?: boolean | null - manageRecords?: boolean | null - canShare?: boolean | null - canEdit?: boolean | null -} + folderUid: string; + folderName?: string | null; + manageUsers?: boolean | null; + manageRecords?: boolean | null; + canShare?: boolean | null; + canEdit?: boolean | null; +}; export type UpdateFolderResult = { - folderUid: string - success: boolean - message?: string -} + folderUid: string; + success: boolean; + message?: string; +}; export type RenameFolderResult = { - folderUid: string - oldName: string - newName: string - success: boolean - message?: string -} + folderUid: string; + oldName: string; + newName: string; + success: boolean; + message?: string; +}; type ResolvedFolder = - | { kind: FolderKind.UserFolder; folder: DUserFolder } - | { kind: FolderKind.SharedFolder; folder: DSharedFolder } - | { kind: FolderKind.SharedFolderFolder; folder: DSharedFolderFolder } - -function resolveFolderEntity(storage: InMemoryStorage, folderUid: string): ResolvedFolder | undefined { - const userFolder = storage.getByUid(FolderKind.UserFolder, folderUid) - if (userFolder) return { kind: FolderKind.UserFolder, folder: userFolder } - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, folderUid) - if (sharedFolder) return { kind: FolderKind.SharedFolder, folder: sharedFolder } - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, folderUid) - if (sharedFolderFolder) return { kind: FolderKind.SharedFolderFolder, folder: sharedFolderFolder } - return undefined + | { kind: FolderKind.UserFolder; folder: DUserFolder } + | { kind: FolderKind.SharedFolder; folder: DSharedFolder } + | { kind: FolderKind.SharedFolderFolder; folder: DSharedFolderFolder }; + +function resolveFolderEntity( + storage: InMemoryStorage, + folderUid: string, +): ResolvedFolder | undefined { + const userFolder = storage.getByUid( + FolderKind.UserFolder, + folderUid, + ); + if (userFolder) return { kind: FolderKind.UserFolder, folder: userFolder }; + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + folderUid, + ); + if (sharedFolder) + return { kind: FolderKind.SharedFolder, folder: sharedFolder }; + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + folderUid, + ); + if (sharedFolderFolder) + return { kind: FolderKind.SharedFolderFolder, folder: sharedFolderFolder }; + return undefined; } -function mergeFolderData(existing: unknown, folderName: string | null | undefined): Record { - const base = isObject(existing) ? { ...existing } : {} - const trimmed = folderName?.trim() - if (trimmed) { - base.title = trimmed - base.name = trimmed - } - return base +function mergeFolderData( + existing: unknown, + folderName: string | null | undefined, +): Record { + const base = isObject(existing) ? { ...existing } : {}; + const trimmed = folderName?.trim(); + if (trimmed) { + base.title = trimmed; + base.name = trimmed; + } + return base; } export async function updateFolder( - auth: Auth, - storage: InMemoryStorage, - input: UpdateFolderInput + auth: Auth, + storage: InMemoryStorage, + input: UpdateFolderInput, ): Promise { - const folderUid = input.folderUid - const resolved = resolveFolderEntity(storage, folderUid) - if (!resolved) { - throw new KeeperSdkError(`Folder "${folderUid}" does not exist.`, 'folder_not_found') - } + const folderUid = input.folderUid; + const resolved = resolveFolderEntity(storage, folderUid); + if (!resolved) { + throw new KeeperSdkError( + `Folder "${folderUid}" does not exist.`, + "folder_not_found", + ); + } - const folderKey = await storage.getKeyBytes(folderUid) - if (!folderKey) { - throw new KeeperSdkError( - 'Folder encryption key not available. Sync the vault and try again.', - 'folder_key_missing' - ) - } + const folderKey = await storage.getKeyBytes(folderUid); + if (!folderKey) { + throw new KeeperSdkError( + "Folder encryption key not available. Sync the vault and try again.", + "folder_key_missing", + ); + } - const trimmedName = input.folderName?.trim() || '' - if (trimmedName) validateFolderName(trimmedName) - - const hasPermissionUpdate = anyIsBoolean( - input.manageUsers, - input.manageRecords, - input.canShare, - input.canEdit - ) - - if (resolved.kind === FolderKind.UserFolder || resolved.kind === FolderKind.SharedFolderFolder) { - if (!trimmedName) { - throw new KeeperSdkError('Folder name is required.', 'folder_name_required') - } - } else if (resolved.kind === FolderKind.SharedFolder) { - if (!trimmedName && !hasPermissionUpdate) { - throw new KeeperSdkError( - 'Provide a new name or at least one permission to update.', - 'shared_folder_update_empty' - ) - } - } + const trimmedName = input.folderName?.trim() || ""; + if (trimmedName) validateFolderName(trimmedName); - const effectiveName = - resolved.kind === FolderKind.SharedFolder ? trimmedName || resolved.folder.name || undefined : trimmedName - const mergedData = mergeFolderData(resolved.folder.data, effectiveName) + const hasPermissionUpdate = anyIsBoolean( + input.manageUsers, + input.manageRecords, + input.canShare, + input.canEdit, + ); - const request: FolderUpdateRequest = { - folder_uid: folderUid, - folder_type: resolved.kind, - data: await encryptObjectForStorage(mergedData, folderKey), + if ( + resolved.kind === FolderKind.UserFolder || + resolved.kind === FolderKind.SharedFolderFolder + ) { + if (!trimmedName) { + throw new KeeperSdkError( + "Folder name is required.", + "folder_name_required", + ); } - - if (resolved.kind === FolderKind.SharedFolder) { - const sharedFolder = resolved.folder - const displayName = trimmedName || sharedFolder.name || folderUid - request.shared_folder_uid = folderUid - request.name = await encryptForStorage(platform.stringToBytes(displayName), folderKey) - request.manage_users = isBoolean(input.manageUsers) ? input.manageUsers : sharedFolder.defaultManageUsers - request.manage_records = isBoolean(input.manageRecords) - ? input.manageRecords - : sharedFolder.defaultManageRecords - request.can_edit = isBoolean(input.canEdit) ? input.canEdit : sharedFolder.defaultCanEdit - request.can_share = isBoolean(input.canShare) ? input.canShare : sharedFolder.defaultCanShare - } else if (resolved.kind === FolderKind.SharedFolderFolder) { - request.shared_folder_uid = resolved.folder.sharedFolderUid + } else if (resolved.kind === FolderKind.SharedFolder) { + if (!trimmedName && !hasPermissionUpdate) { + throw new KeeperSdkError( + "Provide a new name or at least one permission to update.", + "shared_folder_update_empty", + ); } + } - const folderLabel = effectiveName || folderUid - - try { - const response = await auth.executeRestCommand(folderUpdateCommand(request)) - const succeeded = - response.result === FolderResultStatus.Success || response.result_code === FolderResultStatus.Success - if (!succeeded) { - const reason = - response.message || - response.result_code || - `folder_update failed for "${folderLabel}" (uid=${folderUid}, type=${resolved.kind}): server returned no message or result_code` - return { - folderUid, - success: false, - message: reason, - } - } - return { folderUid, success: true } - } catch (err) { - return { - folderUid, - success: false, - message: `folder_update failed for "${folderLabel}" (uid=${folderUid}, type=${resolved.kind}): ${extractErrorMessage(err)}`, - } + const effectiveName = + resolved.kind === FolderKind.SharedFolder + ? trimmedName || resolved.folder.name || undefined + : trimmedName; + const mergedData = mergeFolderData(resolved.folder.data, effectiveName); + + const request: FolderUpdateRequest = { + folder_uid: folderUid, + folder_type: resolved.kind, + data: await encryptObjectForStorage(mergedData, folderKey), + }; + + if (resolved.kind === FolderKind.SharedFolder) { + const sharedFolder = resolved.folder; + const displayName = trimmedName || sharedFolder.name || folderUid; + request.shared_folder_uid = folderUid; + request.name = await encryptForStorage( + platform.stringToBytes(displayName), + folderKey, + ); + request.manage_users = isBoolean(input.manageUsers) + ? input.manageUsers + : sharedFolder.defaultManageUsers; + request.manage_records = isBoolean(input.manageRecords) + ? input.manageRecords + : sharedFolder.defaultManageRecords; + request.can_edit = isBoolean(input.canEdit) + ? input.canEdit + : sharedFolder.defaultCanEdit; + request.can_share = isBoolean(input.canShare) + ? input.canShare + : sharedFolder.defaultCanShare; + } else if (resolved.kind === FolderKind.SharedFolderFolder) { + request.shared_folder_uid = resolved.folder.sharedFolderUid; + } + + const folderLabel = effectiveName || folderUid; + + try { + const response = await auth.executeRestCommand( + folderUpdateCommand(request), + ); + const succeeded = + response.result === FolderResultStatus.Success || + response.result_code === FolderResultStatus.Success; + if (!succeeded) { + const reason = + response.message || + response.result_code || + `folder_update failed for "${folderLabel}" (uid=${folderUid}, type=${resolved.kind}): server returned no message or result_code`; + return { + folderUid, + success: false, + message: reason, + }; } + return { folderUid, success: true }; + } catch (err) { + return { + folderUid, + success: false, + message: `folder_update failed for "${folderLabel}" (uid=${folderUid}, type=${resolved.kind}): ${extractErrorMessage(err)}`, + }; + } } export async function renameFolder( - auth: Auth, - storage: InMemoryStorage, - session: VaultFolderSession, - folderPath: string, - newName: string + auth: Auth, + storage: InMemoryStorage, + session: VaultFolderSession, + folderPath: string, + newName: string, ): Promise { - const trimmedPath = folderPath.trim() - if (!trimmedPath) { - throw new KeeperSdkError('Folder cannot be empty.', 'folder_required') - } - const trimmedName = newName.trim() - if (!trimmedName) { - throw new KeeperSdkError('New folder name is required.', 'folder_name_required') - } + const trimmedPath = folderPath.trim(); + if (!trimmedPath) { + throw new KeeperSdkError("Folder cannot be empty.", "folder_required"); + } + const trimmedName = newName.trim(); + if (!trimmedName) { + throw new KeeperSdkError( + "New folder name is required.", + "folder_name_required", + ); + } - const resolved = await resolveSingleFolder(storage, session, trimmedPath) - if (resolved.folderUid === null) { - throw new KeeperSdkError('Cannot rename the root folder.', 'folder_root_rename') - } + const resolved = await resolveSingleFolder(storage, session, trimmedPath); + if (resolved.folderUid === null) { + throw new KeeperSdkError( + "Cannot rename the root folder.", + "folder_root_rename", + ); + } - const result = await updateFolder(auth, storage, { - folderUid: resolved.folderUid, - folderName: trimmedName, - }) + const result = await updateFolder(auth, storage, { + folderUid: resolved.folderUid, + folderName: trimmedName, + }); - return { - folderUid: resolved.folderUid, - oldName: resolved.name, - newName: trimmedName, - success: result.success, - message: result.message, - } + return { + folderUid: resolved.folderUid, + oldName: resolved.name, + newName: trimmedName, + success: result.success, + message: result.message, + }; } export async function updateSharedFolderPermissions( - auth: Auth, - storage: InMemoryStorage, - sharedFolderUid: string, - permissions: { - manageUsers?: boolean | null - manageRecords?: boolean | null - canShare?: boolean | null - canEdit?: boolean | null - } + auth: Auth, + storage: InMemoryStorage, + sharedFolderUid: string, + permissions: { + manageUsers?: boolean | null; + manageRecords?: boolean | null; + canShare?: boolean | null; + canEdit?: boolean | null; + }, ): Promise { - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, sharedFolderUid) - if (!sharedFolder) { - throw new KeeperSdkError(`"${sharedFolderUid}" is not a shared folder.`, 'not_shared_folder') - } - return updateFolder(auth, storage, { - folderUid: sharedFolderUid, - folderName: null, - ...permissions, - }) + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + sharedFolderUid, + ); + if (!sharedFolder) { + throw new KeeperSdkError( + `"${sharedFolderUid}" is not a shared folder.`, + "not_shared_folder", + ); + } + return updateFolder(auth, storage, { + folderUid: sharedFolderUid, + folderName: null, + ...permissions, + }); } diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 9d1f8156..a4504f33 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -1,463 +1,514 @@ -import { connectSdkPlatform } from './platform' -import { nodeSdkPlatform } from './platform/node/platform' +import { connectSdkPlatform } from "./platform"; +import { nodeSdkPlatform } from "./platform/node/platform"; -connectSdkPlatform(nodeSdkPlatform) +connectSdkPlatform(nodeSdkPlatform); -export * from './api' +export * from "./api"; /** Node.js Commander / CLI helpers (readline, ~/.keeper config). */ -export { ConsoleAuthUI } from './auth/ConsoleAuthUI' -export { FileConfigLoader } from './auth/node/FileConfigLoader' -export { login, cleanup, prompt, suppressLogs, loadKeeperConfig, resolveServer } from './auth/ConsoleLogin' +export { ConsoleAuthUI } from "./auth/ConsoleAuthUI"; +export { FileConfigLoader } from "./auth/node/FileConfigLoader"; +export { + login, + cleanup, + prompt, + suppressLogs, + loadKeeperConfig, + resolveServer, +} from "./auth/ConsoleLogin"; -export { InMemoryStorage } from './storage/InMemoryStorage' +export { InMemoryStorage } from "./storage/InMemoryStorage"; export { - Logger, - ConsoleLogger, - LogLevel, - logger, - setLogger, - getLogger, - resetLogger, - KeeperSdkError, - isKeeperError, - extractErrorMessage, - extractResultCode, - SdkDefaults, - AuthDefaults, - ResultCodes, - AuthErrorCode, - SessionErrorCode, - ValidationErrorCode, - RoleErrorCode, - TeamErrorCode, - UserErrorCode, - KEEPER_PUBLIC_HOSTS, - isBoolean, - isString, - isNonEmptyString, - isNumber, - isObject, - anyIsBoolean, - EMAIL_PATTERN, - EMAIL_LIST_SEPARATOR_PATTERN, - isValidEmail, - resolveSearchPattern, -} from './utils' -export type { ILogger, Nullable, Optional, DeepPartial, Immutable } from './utils' + Logger, + ConsoleLogger, + LogLevel, + logger, + setLogger, + getLogger, + resetLogger, + KeeperSdkError, + isKeeperError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + AuthDefaults, + ResultCodes, + AuthErrorCode, + SessionErrorCode, + ValidationErrorCode, + RoleErrorCode, + TeamErrorCode, + UserErrorCode, + KEEPER_PUBLIC_HOSTS, + isBoolean, + isString, + isNonEmptyString, + isNumber, + isObject, + anyIsBoolean, + EMAIL_PATTERN, + EMAIL_LIST_SEPARATOR_PATTERN, + isValidEmail, + resolveSearchPattern, +} from "./utils"; +export type { + ILogger, + Nullable, + Optional, + DeepPartial, + Immutable, +} from "./utils"; export { - searchRecords, - formatRecord, - getRecordTitle, - getRecordType, - getRecordFields, - getRecordSummary, - getRecordPassword, - getRecordLogin, - getRecordUrl, - getRecordTotpUrl, - RecordVersion, -} from './records/RecordUtils' -export type { RecordSummary } from './records/RecordUtils' -export { parseTotpUrl, getTotpCode } from './records/Totp' -export type { TotpAlgorithm, TotpParams, TotpCode } from './records/Totp' -export { addRecord, updateRecord, deleteRecord, getRecordHistory, moveRecord } from './records/RecordOperations' + searchRecords, + formatRecord, + getRecordTitle, + getRecordType, + getRecordFields, + getRecordSummary, + getRecordPassword, + getRecordLogin, + getRecordUrl, + getRecordTotpUrl, + RecordVersion, +} from "./records/RecordUtils"; +export type { RecordSummary } from "./records/RecordUtils"; +export { parseTotpUrl, getTotpCode } from "./records/Totp"; +export type { TotpAlgorithm, TotpParams, TotpCode } from "./records/Totp"; +export { + addRecord, + updateRecord, + deleteRecord, + getRecordHistory, + moveRecord, +} from "./records/RecordOperations"; export type { - PasswordRecordData, - TypedRecordData, - RecordFieldInput, - NewRecordInput, - AddRecordResult, - UpdateRecordResult, - DeleteRecordResult, - HistoryEntry, - RecordHistoryResult, - MoveRecordInput, - MoveRecordResult, -} from './records/RecordOperations' - -export { shareRecord, removeRecordShare, getRecordShareInfo } from './sharing/Sharing' + PasswordRecordData, + TypedRecordData, + RecordFieldInput, + NewRecordInput, + AddRecordResult, + UpdateRecordResult, + DeleteRecordResult, + HistoryEntry, + RecordHistoryResult, + MoveRecordInput, + MoveRecordResult, +} from "./records/RecordOperations"; + +export { + shareRecord, + removeRecordShare, + getRecordShareInfo, +} from "./sharing/Sharing"; +export type { + ShareRecordInput, + ShareRecordResult, + RemoveShareInput, + RemoveShareResult, + RecordShareInfo, + RecordUserPermission, + RecordSharedFolderPermission, +} from "./sharing/Sharing"; + +export { KeeperVault } from "./vault/KeeperVault"; +export type { KeeperVaultConfig, VaultSummary } from "./vault/KeeperVault"; + +export { getFolder, findFolder, GetFolderFormat } from "./folders/getFolder"; +export type { + GetFolderOptions, + GetFolderResult, + GetFolderResultFolder, + GetFolderResultSharedFolder, + GetFolderFormatInput, + FoundFolder, +} from "./folders/getFolder"; + +export { + FolderKind, + ParentFolderKind, + FolderObjectType, + FolderResultStatus, + DeleteResolution, + DeleteObjectType, + folderKindFromString, +} from "./folders/folderHelpers"; +export type { FolderKindOrLiteral } from "./folders/folderHelpers"; + +export { + listFolder, + findFolderUidByNameOrUid, + listRootUserFolders, +} from "./folders/listFolder"; export type { - ShareRecordInput, - ShareRecordResult, - RemoveShareInput, - RemoveShareResult, - RecordShareInfo, - RecordUserPermission, - RecordSharedFolderPermission, -} from './sharing/Sharing' - -export { KeeperVault } from './vault/KeeperVault' -export type { KeeperVaultConfig, VaultSummary } from './vault/KeeperVault' - -export { getFolder, findFolder, GetFolderFormat } from './folders/getFolder' + ListFolderOptions, + ListFolderResult, + ListFolderFolderSimple, + ListFolderRecordSimple, + ListFolderFolderDetail, + ListFolderRecordDetail, +} from "./folders/listFolder"; + +export { + listSharedFolders, + formatSharedFoldersTable, + renderSharedFoldersAsciiTable, +} from "./sharedFolders/listSharedFolders"; export type { - GetFolderOptions, - GetFolderResult, - GetFolderResultFolder, - GetFolderResultSharedFolder, - GetFolderFormatInput, - FoundFolder, -} from './folders/getFolder' + ListSharedFoldersOptions, + ListSharedFolderRow, + FormattedSharedFoldersTable, +} from "./sharedFolders/listSharedFolders"; export { - FolderKind, - ParentFolderKind, - FolderObjectType, - FolderResultStatus, - DeleteResolution, - DeleteObjectType, - folderKindFromString, -} from './folders/folderHelpers' -export type { FolderKindOrLiteral } from './folders/folderHelpers' - -export { listFolder, findFolderUidByNameOrUid, listRootUserFolders } from './folders/listFolder' + shareFolder, + ShareFolderAction, + ShareFolderUserResultStatus, +} from "./sharedFolders/shareFolder"; export type { - ListFolderOptions, - ListFolderResult, - ListFolderFolderSimple, - ListFolderRecordSimple, - ListFolderFolderDetail, - ListFolderRecordDetail, -} from './folders/listFolder' + ShareFolderActionInput, + ShareFolderInput, + ShareFolderResult, + ShareFolderUserStatus, +} from "./sharedFolders/shareFolder"; export { - listSharedFolders, - formatSharedFoldersTable, - renderSharedFoldersAsciiTable, -} from './sharedFolders/listSharedFolders' + changeDirectory, + createVaultFolderSession, + tryResolvePath, + resolveSingleFolder, + getWorkingFolderDisplayName, + findParentFolderUid, + splitPathComponents, +} from "./folders/changeDirectory"; export type { - ListSharedFoldersOptions, - ListSharedFolderRow, - FormattedSharedFoldersTable, -} from './sharedFolders/listSharedFolders' + VaultFolderSession, + ChangeDirectoryResult, + TryResolvePathResult, +} from "./folders/changeDirectory"; -export { shareFolder, ShareFolderAction, ShareFolderUserResultStatus } from './sharedFolders/shareFolder' +export { addFolder, mkdir } from "./folders/addFolder"; export type { - ShareFolderActionInput, - ShareFolderInput, - ShareFolderResult, - ShareFolderUserStatus, -} from './sharedFolders/shareFolder' + AddFolderInput, + AddFolderResult, + MkdirOptions, +} from "./folders/addFolder"; export { - changeDirectory, - createVaultFolderSession, - tryResolvePath, - resolveSingleFolder, - getWorkingFolderDisplayName, - findParentFolderUid, - splitPathComponents, -} from './folders/changeDirectory' -export type { VaultFolderSession, ChangeDirectoryResult, TryResolvePathResult } from './folders/changeDirectory' - -export { addFolder, mkdir } from './folders/addFolder' -export type { AddFolderInput, AddFolderResult, MkdirOptions } from './folders/addFolder' - -export { updateFolder, renameFolder, updateSharedFolderPermissions } from './folders/updateFolder' -export type { UpdateFolderInput, UpdateFolderResult, RenameFolderResult } from './folders/updateFolder' + updateFolder, + renameFolder, + updateSharedFolderPermissions, +} from "./folders/updateFolder"; +export type { + UpdateFolderInput, + UpdateFolderResult, + RenameFolderResult, +} from "./folders/updateFolder"; export { - deleteFolder, - rmdir, - resolveRmdirPatternsToFolderUids, - buildFolderDeleteObject, -} from './folders/deleteFolder' -export type { DeleteFolderResult, RmdirOptions } from './folders/deleteFolder' + deleteFolder, + rmdir, + resolveRmdirPatternsToFolderUids, + buildFolderDeleteObject, +} from "./folders/deleteFolder"; +export type { DeleteFolderResult, RmdirOptions } from "./folders/deleteFolder"; export { - buildFolderTree, - renderFolderTreeAscii, - folderTreeAscii, - userPermissionToText, - recordPermissionToText, -} from './folders/folderTree' -export type { FolderTreeBuildOptions, FolderTreeNode, FolderTreeResult } from './folders/folderTree' + buildFolderTree, + renderFolderTreeAscii, + folderTreeAscii, + userPermissionToText, + recordPermissionToText, +} from "./folders/folderTree"; +export type { + FolderTreeBuildOptions, + FolderTreeNode, + FolderTreeResult, +} from "./folders/folderTree"; -export { FolderManager } from './folders/FolderManager' -export type { AuthProvider, SharedFolderPermissionsInput } from './folders/FolderManager' +export { FolderManager } from "./folders/FolderManager"; +export type { + AuthProvider, + SharedFolderPermissionsInput, +} from "./folders/FolderManager"; -export { SharedFolderManager } from './sharedFolders/SharedFolderManager' +export { SharedFolderManager } from "./sharedFolders/SharedFolderManager"; export { - listTeams, - formatTeamsTable, - renderTeamsAsciiTable, - formatTeamRestricts, - TeamColumn, - SUPPORTED_TEAM_COLUMNS, - DEFAULT_TEAM_COLUMNS, -} from './teams/listTeams' + listTeams, + formatTeamsTable, + renderTeamsAsciiTable, + formatTeamRestricts, + TeamColumn, + SUPPORTED_TEAM_COLUMNS, + DEFAULT_TEAM_COLUMNS, +} from "./teams/listTeams"; export type { - ListTeamsOptions, - ListTeamRow, - ListTeamSort, - TeamColumnInput, - FormattedTeamsTable, - FormatTeamsTableOptions, -} from './teams/listTeams' + ListTeamsOptions, + ListTeamRow, + ListTeamSort, + TeamColumnInput, + FormattedTeamsTable, + FormatTeamsTableOptions, +} from "./teams/listTeams"; export { - EnterpriseDataInclude, - EnterpriseDataManager, -} from './teams/enterpriseData' + EnterpriseDataInclude, + EnterpriseDataManager, +} from "./teams/enterpriseData"; export type { - EnterpriseDataManagerApi, - GetEnterpriseDataResponse, - EnterpriseTeamRecord, - EnterpriseTeamUserLink, - EnterpriseRoleUserLink, - EnterpriseRoleTeamLink, - EnterpriseRolePrivilegeLink, - EnterpriseRoleManagedNodeLink, - EnterpriseRoleEnforcementLink, - EnterpriseQueuedTeamRecord, - EnterpriseQueuedTeamUserLink, - EnterpriseUserAliasLink, - EnterpriseUser, - EnterpriseRole, - EnterpriseNode, - DecryptedNodeNames, - DecryptedRoleNames, - EnterpriseDisplayNames, - NodePathOptions, -} from './teams/enterpriseData' + EnterpriseDataManagerApi, + GetEnterpriseDataResponse, + EnterpriseTeamRecord, + EnterpriseTeamUserLink, + EnterpriseRoleUserLink, + EnterpriseRoleTeamLink, + EnterpriseRolePrivilegeLink, + EnterpriseRoleManagedNodeLink, + EnterpriseRoleEnforcementLink, + EnterpriseQueuedTeamRecord, + EnterpriseQueuedTeamUserLink, + EnterpriseUserAliasLink, + EnterpriseUser, + EnterpriseRole, + EnterpriseNode, + DecryptedNodeNames, + DecryptedRoleNames, + EnterpriseDisplayNames, + NodePathOptions, +} from "./teams/enterpriseData"; export { - listRoles, - formatRolesTable, - renderRolesAsciiTable, - RoleColumn, - SUPPORTED_ROLE_COLUMNS, - DEFAULT_ROLE_COLUMNS, - ALL_COLUMNS_WILDCARD, - viewRole, - formatRoleView, - roleViewTable, - RoleManager, - addRoles, - formatAddRoleResult, - renderAddRoleAsciiTable, - AddRoleStatus, - AddRoleSkipReason, - updateRoles, - formatUpdateRoleResult, - renderUpdateRoleAsciiTable, - UpdateRoleStatus, - deleteRoles, - formatDeleteRoleResult, - renderDeleteRoleAsciiTable, - DeleteRoleStatus, -} from './roles' + listRoles, + formatRolesTable, + renderRolesAsciiTable, + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + DEFAULT_ROLE_COLUMNS, + ALL_COLUMNS_WILDCARD, + viewRole, + formatRoleView, + roleViewTable, + RoleManager, + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + AddRoleStatus, + AddRoleSkipReason, + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + UpdateRoleStatus, + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + DeleteRoleStatus, +} from "./roles"; export type { - ListRolesOptions, - ListRoleRow, - RoleColumnInput, - FormattedRolesTable, - FormatRolesTableOptions, - RoleView, - RoleTeamInfo, - RoleUserInfo, - RoleManagedNodeInfo, - RoleEnforcementInfo, - FormatRoleViewOptions, - FormattedRoleViewTable, - FormattedManagedNodePrivilegeTable, - RoleViewTableRow, - AddRoleInput, - AddRoleResult, - AddRoleItemResult, - AddRoleConfirm, - AddRoleConflictPrompt, - FormattedAddRoleTable, - UpdateRoleInput, - UpdateRoleResult, - UpdateRoleItemResult, - FormattedUpdateRoleTable, - DeleteRoleInput, - DeleteRoleResult, - DeleteRoleItemResult, - FormattedDeleteRoleTable, - RoleToggleInput, - RoleToggle, - EnforcementPair, -} from './roles' - -export { viewTeam, formatTeamView, teamViewTable } from './teams/viewTeam' + ListRolesOptions, + ListRoleRow, + RoleColumnInput, + FormattedRolesTable, + FormatRolesTableOptions, + RoleView, + RoleTeamInfo, + RoleUserInfo, + RoleManagedNodeInfo, + RoleEnforcementInfo, + FormatRoleViewOptions, + FormattedRoleViewTable, + FormattedManagedNodePrivilegeTable, + RoleViewTableRow, + AddRoleInput, + AddRoleResult, + AddRoleItemResult, + AddRoleConfirm, + AddRoleConflictPrompt, + FormattedAddRoleTable, + UpdateRoleInput, + UpdateRoleResult, + UpdateRoleItemResult, + FormattedUpdateRoleTable, + DeleteRoleInput, + DeleteRoleResult, + DeleteRoleItemResult, + FormattedDeleteRoleTable, + RoleToggleInput, + RoleToggle, + EnforcementPair, +} from "./roles"; + +export { viewTeam, formatTeamView, teamViewTable } from "./teams/viewTeam"; export type { - TeamView, - TeamRoleInfo, - TeamUserInfo, - FormatTeamViewOptions, - FormattedTeamViewTable, - TeamViewTableRow, -} from './teams/viewTeam' + TeamView, + TeamRoleInfo, + TeamUserInfo, + FormatTeamViewOptions, + FormattedTeamViewTable, + TeamViewTableRow, +} from "./teams/viewTeam"; export { - addTeams, - formatAddTeamResult, - renderAddTeamAsciiTable, - AddTeamSourceKind, - AddTeamSkipReason, - AddTeamStatus, - TeamRestriction, -} from './teams/addTeam' + addTeams, + formatAddTeamResult, + renderAddTeamAsciiTable, + AddTeamSourceKind, + AddTeamSkipReason, + AddTeamStatus, + TeamRestriction, +} from "./teams/addTeam"; export type { - AddTeamInput, - AddTeamResult, - AddTeamItemResult, - AddTeamConfirm, - AddTeamConflictPrompt, - TeamRestrictionInput, - FormatAddTeamResultOptions, - FormattedAddTeamTable, - FormattedAddTeamRow, -} from './teams/addTeam' + AddTeamInput, + AddTeamResult, + AddTeamItemResult, + AddTeamConfirm, + AddTeamConflictPrompt, + TeamRestrictionInput, + FormatAddTeamResultOptions, + FormattedAddTeamTable, + FormattedAddTeamRow, +} from "./teams/addTeam"; export { - updateTeams, - formatUpdateTeamResult, - renderUpdateTeamAsciiTable, - UpdateTeamStatus, -} from './teams/updateTeam' + updateTeams, + formatUpdateTeamResult, + renderUpdateTeamAsciiTable, + UpdateTeamStatus, +} from "./teams/updateTeam"; export type { - UpdateTeamInput, - UpdateTeamResult, - UpdateTeamItemResult, - FormattedUpdateTeamTable, - FormattedUpdateTeamRow, -} from './teams/updateTeam' + UpdateTeamInput, + UpdateTeamResult, + UpdateTeamItemResult, + FormattedUpdateTeamTable, + FormattedUpdateTeamRow, +} from "./teams/updateTeam"; export { - deleteTeams, - formatDeleteTeamResult, - renderDeleteTeamAsciiTable, - DeleteTeamStatus, -} from './teams/deleteTeam' + deleteTeams, + formatDeleteTeamResult, + renderDeleteTeamAsciiTable, + DeleteTeamStatus, +} from "./teams/deleteTeam"; export type { - DeleteTeamInput, - DeleteTeamResult, - DeleteTeamItemResult, - FormattedDeleteTeamTable, - FormattedDeleteTeamRow, -} from './teams/deleteTeam' + DeleteTeamInput, + DeleteTeamResult, + DeleteTeamItemResult, + FormattedDeleteTeamTable, + FormattedDeleteTeamRow, +} from "./teams/deleteTeam"; -export { TeamManager } from './teams/TeamManager' +export { TeamManager } from "./teams/TeamManager"; export { - listUsers, - formatUsersTable, - renderUsersAsciiTable, - UserColumn, - SUPPORTED_USER_COLUMNS, - DEFAULT_USER_COLUMNS, -} from './users/listUsers' + listUsers, + formatUsersTable, + renderUsersAsciiTable, + UserColumn, + SUPPORTED_USER_COLUMNS, + DEFAULT_USER_COLUMNS, +} from "./users/listUsers"; -export { viewUser, formatUserView, userViewTable } from './users/viewUser' +export { viewUser, formatUserView, userViewTable } from "./users/viewUser"; export { - addUsers, - formatAddUserResult, - renderAddUserAsciiTable, - AddUserStatus, - AddUserSkipReason, -} from './users/addUser' + addUsers, + formatAddUserResult, + renderAddUserAsciiTable, + AddUserStatus, + AddUserSkipReason, +} from "./users/addUser"; export { - updateUsers, - formatUpdateUserResult, - renderUpdateUserAsciiTable, - UpdateUserStatus, -} from './users/updateUser' + updateUsers, + formatUpdateUserResult, + renderUpdateUserAsciiTable, + UpdateUserStatus, +} from "./users/updateUser"; export { - deleteUsers, - formatDeleteUserResult, - renderDeleteUserAsciiTable, - DeleteUserStatus, -} from './users/deleteUser' + deleteUsers, + formatDeleteUserResult, + renderDeleteUserAsciiTable, + DeleteUserStatus, +} from "./users/deleteUser"; export { - actionUsers, - formatUserActionResult, - renderUserActionAsciiTable, - UserAction, - UserActionStatus, - UserActionSkipReason, -} from './users/actionUser' + actionUsers, + formatUserActionResult, + renderUserActionAsciiTable, + UserAction, + UserActionStatus, + UserActionSkipReason, +} from "./users/actionUser"; export type { - UserColumnInput, - ListUsersOptions, - ListUserRow, - FormattedUsersTable, - FormatUsersTableOptions, - UserTeamInfo, - UserRoleInfo, - UserView, - FormatUserViewOptions, - FormattedUserViewTable, - UserViewTableRow, - AddUserInput, - AddUserItemResult, - AddUserResult, - FormatAddUserResultOptions, - FormattedAddUserTable, - UpdateUserInput, - UpdateUserItemResult, - UpdateUserResult, - FormattedUpdateUserTable, - DeleteUserInput, - DeleteUserItemResult, - DeleteUserResult, - FormattedDeleteUserTable, - UserActionInput, - UserActionItemResult, - UserActionResult, - FormattedUserActionTable, - AliasUserInput, - AliasUserResult, - FormattedUserStatus, - AddUsersToTeamsInput, - RemoveUsersFromTeamsInput, - TeamUserItemResult, - TeamUserResult, - FormattedTeamUserTable, -} from './users/userTypes' - -export { EnterpriseUserStatus } from './users/userTypes' - -export { - aliasUser, - AliasOperation, -} from './users/aliasUser' + UserColumnInput, + ListUsersOptions, + ListUserRow, + FormattedUsersTable, + FormatUsersTableOptions, + UserTeamInfo, + UserRoleInfo, + UserView, + FormatUserViewOptions, + FormattedUserViewTable, + UserViewTableRow, + AddUserInput, + AddUserItemResult, + AddUserResult, + FormatAddUserResultOptions, + FormattedAddUserTable, + UpdateUserInput, + UpdateUserItemResult, + UpdateUserResult, + FormattedUpdateUserTable, + DeleteUserInput, + DeleteUserItemResult, + DeleteUserResult, + FormattedDeleteUserTable, + UserActionInput, + UserActionItemResult, + UserActionResult, + FormattedUserActionTable, + AliasUserInput, + AliasUserResult, + FormattedUserStatus, + AddUsersToTeamsInput, + RemoveUsersFromTeamsInput, + TeamUserItemResult, + TeamUserResult, + FormattedTeamUserTable, +} from "./users/userTypes"; + +export { EnterpriseUserStatus } from "./users/userTypes"; + +export { aliasUser, AliasOperation } from "./users/aliasUser"; export { - addUsersToTeams, - removeUsersFromTeams, - formatTeamUserResult, - renderTeamUserAsciiTable, - TeamUserStatus, - TeamUserSkipReason, -} from './users/teamUser' + addUsersToTeams, + removeUsersFromTeams, + formatTeamUserResult, + renderTeamUserAsciiTable, + TeamUserStatus, + TeamUserSkipReason, +} from "./users/teamUser"; -export { UserManager } from './users/UserManager' +export { UserManager } from "./users/UserManager"; export type { - DRecord, - DRecordMetadata, - DSharedFolder, - DTeam, - DUserFolder, - VaultStorage, - SyncResult, - SyncDownOptions, - ClientConfiguration, - DeviceConfig, - SessionStorage, - AuthUI3, - KeeperError, - LoginError, -} from '@keeper-security/keeperapi' \ No newline at end of file + DRecord, + DRecordMetadata, + DSharedFolder, + DTeam, + DUserFolder, + VaultStorage, + SyncResult, + SyncDownOptions, + ClientConfiguration, + DeviceConfig, + SessionStorage, + AuthUI3, + KeeperError, + LoginError, +} from "@keeper-security/keeperapi"; diff --git a/KeeperSdk/src/platform/browser/platform.ts b/KeeperSdk/src/platform/browser/platform.ts index 95938970..7e331185 100644 --- a/KeeperSdk/src/platform/browser/platform.ts +++ b/KeeperSdk/src/platform/browser/platform.ts @@ -1,76 +1,84 @@ -import type { AuthUI3 } from '@keeper-security/keeperapi' -import { UnavailableAuthUI } from '../../auth/UnavailableAuthUI' -import { KeeperSdkError, ResultCodes } from '../../utils' -import type { ConfigLoader } from '../../auth/config' -import * as asmCrypto from 'asmcrypto.js' -import type { SdkPlatform, SdkReadline } from '../types' +import type { AuthUI3 } from "@keeper-security/keeperapi"; +import { UnavailableAuthUI } from "../../auth/UnavailableAuthUI"; +import { KeeperSdkError, ResultCodes } from "../../utils"; +import type { ConfigLoader } from "../../auth/config"; +import * as asmCrypto from "asmcrypto.js"; +import type { SdkPlatform, SdkReadline } from "../types"; type HmacCtor = new (key: Uint8Array) => { - process(data: Uint8Array): void - finish(): void - result: Uint8Array -} + process(data: Uint8Array): void; + finish(): void; + result: Uint8Array; +}; const asm = asmCrypto as typeof asmCrypto & { - HmacSha1: HmacCtor - HmacSha256: HmacCtor - HmacSha512: HmacCtor -} + HmacSha1: HmacCtor; + HmacSha256: HmacCtor; + HmacSha512: HmacCtor; +}; -const HMAC_IMPL: Record<'sha1' | 'sha256' | 'sha512', HmacCtor> = { - sha1: asm.HmacSha1, - sha256: asm.HmacSha256, - sha512: asm.HmacSha512, -} +const HMAC_IMPL: Record<"sha1" | "sha256" | "sha512", HmacCtor> = { + sha1: asm.HmacSha1, + sha256: asm.HmacSha256, + sha512: asm.HmacSha512, +}; const BROWSER_READLINE_MSG = - 'Interactive readline is not available in the browser. Use keeper-shell password transport or a custom authUI.' + "Interactive readline is not available in the browser. Use keeper-shell password transport or a custom authUI."; const BROWSER_FILE_CONFIG_MSG = - 'File-based Keeper config (~/.keeper) is not available in the browser. Pass an in-memory ConfigLoader to SessionManager or KeeperVault.sessionStorage.' + "File-based Keeper config (~/.keeper) is not available in the browser. Pass an in-memory ConfigLoader to SessionManager or KeeperVault.sessionStorage."; class BrowserReadline implements SdkReadline { - question(_prompt: string): Promise { - return Promise.reject(new KeeperSdkError(BROWSER_READLINE_MSG, ResultCodes.USER_CANCELLED)) - } - close(): void { - /* noop */ - } + question(_prompt: string): Promise { + return Promise.reject( + new KeeperSdkError(BROWSER_READLINE_MSG, ResultCodes.USER_CANCELLED), + ); + } + close(): void { + /* noop */ + } } export const browserSdkPlatform: SdkPlatform = { - runtime: 'browser', + runtime: "browser", - delay(ms: number): Promise { - return new Promise((resolve) => globalThis.setTimeout(resolve, ms)) - }, + delay(ms: number): Promise { + return new Promise((resolve) => globalThis.setTimeout(resolve, ms)); + }, - createReadline(): SdkReadline { - return new BrowserReadline() - }, + createReadline(): SdkReadline { + return new BrowserReadline(); + }, - hmac(algorithm, key, data) { - const Ctor = HMAC_IMPL[algorithm] - if (!Ctor) { - throw new KeeperSdkError(`Unsupported HMAC algorithm: ${algorithm}`, ResultCodes.UNSUPPORTED_2FA_CHANNEL) - } - const h = new Ctor(key) - h.process(data) - h.finish() - return h.result - }, + hmac(algorithm, key, data) { + const Ctor = HMAC_IMPL[algorithm]; + if (!Ctor) { + throw new KeeperSdkError( + `Unsupported HMAC algorithm: ${algorithm}`, + ResultCodes.UNSUPPORTED_2FA_CHANNEL, + ); + } + const h = new Ctor(key); + h.process(data); + h.finish(); + return h.result; + }, - createFileConfigLoader(): ConfigLoader { - throw new KeeperSdkError(BROWSER_FILE_CONFIG_MSG, ResultCodes.NOT_LOGGED_IN) - }, + createFileConfigLoader(): ConfigLoader { + throw new KeeperSdkError( + BROWSER_FILE_CONFIG_MSG, + ResultCodes.NOT_LOGGED_IN, + ); + }, - createAuthUI(useConsoleAuth: boolean): AuthUI3 { - if (useConsoleAuth) { - throw new KeeperSdkError( - 'ConsoleAuthUI (readline) is not available in the browser. Set useConsoleAuth: false and provide authUI, or use keeper-shell.', - ResultCodes.USER_CANCELLED - ) - } - return new UnavailableAuthUI() - }, -} + createAuthUI(useConsoleAuth: boolean): AuthUI3 { + if (useConsoleAuth) { + throw new KeeperSdkError( + "ConsoleAuthUI (readline) is not available in the browser. Set useConsoleAuth: false and provide authUI, or use keeper-shell.", + ResultCodes.USER_CANCELLED, + ); + } + return new UnavailableAuthUI(); + }, +}; diff --git a/KeeperSdk/src/platform/index.ts b/KeeperSdk/src/platform/index.ts index 32bac290..6b47cbfd 100644 --- a/KeeperSdk/src/platform/index.ts +++ b/KeeperSdk/src/platform/index.ts @@ -1,20 +1,22 @@ -import type { SdkPlatform } from './types' +import type { SdkPlatform } from "./types"; -let active: SdkPlatform | undefined +let active: SdkPlatform | undefined; -export type { SdkPlatform, SdkReadline, SdkRuntime } from './types' +export type { SdkPlatform, SdkReadline, SdkRuntime } from "./types"; export function connectSdkPlatform(platform: SdkPlatform): void { - active = platform + active = platform; } export function getSdkPlatform(): SdkPlatform { - if (!active) { - throw new Error('Keeper SDK platform is not initialized. Import @keeper-security/keeper-sdk-javascript or /browser entry first.') - } - return active + if (!active) { + throw new Error( + "Keeper SDK platform is not initialized. Import @keeper-security/keeper-sdk-javascript or /browser entry first.", + ); + } + return active; } export function isSdkPlatformConnected(): boolean { - return active !== undefined + return active !== undefined; } diff --git a/KeeperSdk/src/platform/node/platform.ts b/KeeperSdk/src/platform/node/platform.ts index 9a3a9b60..15b0d6cb 100644 --- a/KeeperSdk/src/platform/node/platform.ts +++ b/KeeperSdk/src/platform/node/platform.ts @@ -1,42 +1,44 @@ -import { createHmac } from 'crypto' -import readline from 'readline/promises' -import type { AuthUI3 } from '@keeper-security/keeperapi' -import { ConsoleAuthUI } from '../../auth/ConsoleAuthUI' -import { UnavailableAuthUI } from '../../auth/UnavailableAuthUI' -import { FileConfigLoader } from '../../auth/node/FileConfigLoader' -import type { ConfigLoader } from '../../auth/config' -import type { SdkPlatform, SdkReadline } from '../types' +import { createHmac } from "crypto"; +import readline from "readline/promises"; +import type { AuthUI3 } from "@keeper-security/keeperapi"; +import { ConsoleAuthUI } from "../../auth/ConsoleAuthUI"; +import { UnavailableAuthUI } from "../../auth/UnavailableAuthUI"; +import { FileConfigLoader } from "../../auth/node/FileConfigLoader"; +import type { ConfigLoader } from "../../auth/config"; +import type { SdkPlatform, SdkReadline } from "../types"; function nodeReadline(input?: unknown, output?: unknown): SdkReadline { - const rl = readline.createInterface({ - input: (input ?? process.stdin) as NodeJS.ReadableStream, - output: (output ?? process.stdout) as NodeJS.WritableStream, - }) - return { - question: (prompt) => rl.question(prompt), - close: () => rl.close(), - } + const rl = readline.createInterface({ + input: (input ?? process.stdin) as NodeJS.ReadableStream, + output: (output ?? process.stdout) as NodeJS.WritableStream, + }); + return { + question: (prompt) => rl.question(prompt), + close: () => rl.close(), + }; } export const nodeSdkPlatform: SdkPlatform = { - runtime: 'node', + runtime: "node", - delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) - }, + delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + }, - createReadline: nodeReadline, + createReadline: nodeReadline, - hmac(algorithm, key, data) { - const algo = algorithm.toLowerCase() - return new Uint8Array(createHmac(algo, Buffer.from(key)).update(data).digest()) - }, + hmac(algorithm, key, data) { + const algo = algorithm.toLowerCase(); + return new Uint8Array( + createHmac(algo, Buffer.from(key)).update(data).digest(), + ); + }, - createFileConfigLoader(configDir?: string): ConfigLoader { - return new FileConfigLoader(configDir) - }, + createFileConfigLoader(configDir?: string): ConfigLoader { + return new FileConfigLoader(configDir); + }, - createAuthUI(useConsoleAuth: boolean): AuthUI3 { - return useConsoleAuth ? new ConsoleAuthUI() : new UnavailableAuthUI() - }, -} + createAuthUI(useConsoleAuth: boolean): AuthUI3 { + return useConsoleAuth ? new ConsoleAuthUI() : new UnavailableAuthUI(); + }, +}; diff --git a/KeeperSdk/src/platform/types.ts b/KeeperSdk/src/platform/types.ts index 7816e006..a9087a04 100644 --- a/KeeperSdk/src/platform/types.ts +++ b/KeeperSdk/src/platform/types.ts @@ -1,25 +1,29 @@ -import type { AuthUI3 } from '@keeper-security/keeperapi' -import type { ConfigLoader } from '../auth/config' +import type { AuthUI3 } from "@keeper-security/keeperapi"; +import type { ConfigLoader } from "../auth/config"; -export type SdkRuntime = 'node' | 'browser' +export type SdkRuntime = "node" | "browser"; export interface SdkReadline { - question(prompt: string): Promise - close(): void + question(prompt: string): Promise; + close(): void; } /** Small platform surface for KeeperSdk (auth UI, config, TOTP, timers). */ export interface SdkPlatform { - readonly runtime: SdkRuntime + readonly runtime: SdkRuntime; - delay(ms: number): Promise + delay(ms: number): Promise; - createReadline(input?: unknown, output?: unknown): SdkReadline + createReadline(input?: unknown, output?: unknown): SdkReadline; - /** HMAC digest (e.g. SHA-1 for TOTP). */ - hmac(algorithm: 'sha1' | 'sha256' | 'sha512', key: Uint8Array, data: Uint8Array): Uint8Array + /** HMAC digest (e.g. SHA-1 for TOTP). */ + hmac( + algorithm: "sha1" | "sha256" | "sha512", + key: Uint8Array, + data: Uint8Array, + ): Uint8Array; - createFileConfigLoader(configDir?: string): ConfigLoader + createFileConfigLoader(configDir?: string): ConfigLoader; - createAuthUI(useConsoleAuth: boolean): AuthUI3 + createAuthUI(useConsoleAuth: boolean): AuthUI3; } diff --git a/KeeperSdk/src/records/RecordOperations.ts b/KeeperSdk/src/records/RecordOperations.ts index acee9afa..b03749b4 100644 --- a/KeeperSdk/src/records/RecordOperations.ts +++ b/KeeperSdk/src/records/RecordOperations.ts @@ -1,549 +1,607 @@ import { - Auth, - Records, - platform, - generateEncryptionKey, - generateUidBytes, - webSafe64FromBytes, - recordsAddMessage, - recordsUpdateMessage, - recordPreDeleteCommand, - recordDeleteCommand, - recordAddCommand, - moveCommand, - getRecordHistoryCommand, - normal64Bytes, -} from '@keeper-security/keeperapi' + Auth, + Records, + platform, + generateEncryptionKey, + generateUidBytes, + webSafe64FromBytes, + recordsAddMessage, + recordsUpdateMessage, + recordPreDeleteCommand, + recordDeleteCommand, + recordAddCommand, + moveCommand, + getRecordHistoryCommand, + normal64Bytes, +} from "@keeper-security/keeperapi"; import type { - DSharedFolder, - DSharedFolderFolder, - DSharedFolderRecord, - DUserFolder, - DRecord, - KeeperPreDeleteResponse, - MoveObject, - MoveRequest, - TransitionKeyObject, - RecordPreDeleteObject, - GetRecordHistoryResponse, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, logger } from '../utils' -import { RecordVersion } from './RecordUtils' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { DeleteResolution, FolderKind, VaultObjectKind } from '../folders/folderHelpers' + DSharedFolder, + DSharedFolderFolder, + DSharedFolderRecord, + DUserFolder, + DRecord, + KeeperPreDeleteResponse, + MoveObject, + MoveRequest, + TransitionKeyObject, + RecordPreDeleteObject, + GetRecordHistoryResponse, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, logger } from "../utils"; +import { RecordVersion } from "./RecordUtils"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { + DeleteResolution, + FolderKind, + VaultObjectKind, +} from "../folders/folderHelpers"; enum ResultCode { - Success = 'success', - OK = 'OK', + Success = "success", + OK = "OK", } -const MIN_RECORD_PAD_BYTES = 384 -const PAD_BLOCK_SIZE = 16 +const MIN_RECORD_PAD_BYTES = 384; +const PAD_BLOCK_SIZE = 16; function getPaddedJsonBytes(data: Record): Uint8Array { - const json = JSON.stringify(data) - const paddedLength = Math.ceil(Math.max(MIN_RECORD_PAD_BYTES, json.length) / PAD_BLOCK_SIZE) * PAD_BLOCK_SIZE - const padded = json.padEnd(paddedLength, ' ') - return new TextEncoder().encode(padded) + const json = JSON.stringify(data); + const paddedLength = + Math.ceil(Math.max(MIN_RECORD_PAD_BYTES, json.length) / PAD_BLOCK_SIZE) * + PAD_BLOCK_SIZE; + const padded = json.padEnd(paddedLength, " "); + return new TextEncoder().encode(padded); } export type PasswordRecordData = { - title: string - login?: string - password?: string - url?: string - notes?: string - custom?: { name: string; value: string; type?: string }[] - totp?: string -} - -function buildLegacyPasswordExtra(data: PasswordRecordData, recordUid: string): Record { - const totp = data.totp?.trim() - if (!totp) { - return {} - } - return { - files: [], - fields: [ - { - id: recordUid, - field_type: 'totp', - field_title: 'One-Time Password', - data: totp, - }, - ], - } + title: string; + login?: string; + password?: string; + url?: string; + notes?: string; + custom?: { name: string; value: string; type?: string }[]; + totp?: string; +}; + +function buildLegacyPasswordExtra( + data: PasswordRecordData, + recordUid: string, +): Record { + const totp = data.totp?.trim(); + if (!totp) { + return {}; + } + return { + files: [], + fields: [ + { + id: recordUid, + field_type: "totp", + field_title: "One-Time Password", + data: totp, + }, + ], + }; } export type RecordFieldInput = { - type: string - value: any[] - label?: string -} + type: string; + value: any[]; + label?: string; +}; export type TypedRecordData = { - type: string - title: string - fields?: RecordFieldInput[] - custom?: RecordFieldInput[] - notes?: string -} + type: string; + title: string; + fields?: RecordFieldInput[]; + custom?: RecordFieldInput[]; + notes?: string; +}; export type NewRecordInput = - | { version: 2; data: PasswordRecordData; folderUid?: string } - | { version: 3; data: TypedRecordData; folderUid?: string } + | { version: 2; data: PasswordRecordData; folderUid?: string } + | { version: 3; data: TypedRecordData; folderUid?: string }; export type AddRecordResult = { - recordUid: string - success: boolean - status?: string -} + recordUid: string; + success: boolean; + status?: string; +}; export type UpdateRecordResult = { - recordUid: string - success: boolean - status?: string -} + recordUid: string; + success: boolean; + status?: string; +}; export type DeleteRecordResult = { - recordUid: string - success: boolean - message?: string -} + recordUid: string; + success: boolean; + message?: string; +}; export async function addRecord( - auth: Auth, - input: NewRecordInput + auth: Auth, + input: NewRecordInput, ): Promise { - if (!input.data.title || !input.data.title.trim()) { - throw new KeeperSdkError('Record title is required.', 'missing_record_title') - } - if (input.version === 3 && !input.data.type?.trim()) { - throw new KeeperSdkError('Record type is required for v3 records.', 'missing_record_type') - } - if (input.version === 2) { - return addPasswordRecord(auth, input.data, input.folderUid) - } - return addTypedRecord(auth, input.data, input.folderUid) + if (!input.data.title || !input.data.title.trim()) { + throw new KeeperSdkError( + "Record title is required.", + "missing_record_title", + ); + } + if (input.version === 3 && !input.data.type?.trim()) { + throw new KeeperSdkError( + "Record type is required for v3 records.", + "missing_record_type", + ); + } + if (input.version === 2) { + return addPasswordRecord(auth, input.data, input.folderUid); + } + return addTypedRecord(auth, input.data, input.folderUid); } async function addPasswordRecord( - auth: Auth, - data: PasswordRecordData, - folderUid?: string + auth: Auth, + data: PasswordRecordData, + folderUid?: string, ): Promise { - const recordUidBytes = generateUidBytes() - const recordKey = generateEncryptionKey() - const recordUid = webSafe64FromBytes(recordUidBytes) - - const recordDataJson = JSON.stringify({ - title: data.title || '', - secret1: data.login || '', - secret2: data.password || '', - link: data.url || '', - notes: data.notes || '', - custom: (data.custom || []).map((c) => ({ - name: c.name, - value: c.value, - type: c.type || 'text', - })), - }) - - const extraJson = JSON.stringify(buildLegacyPasswordExtra(data, recordUid)) - - const dataBytes = new TextEncoder().encode(recordDataJson) - const extraBytes = new TextEncoder().encode(extraJson) - - const encryptedData = await platform.aesCbcEncrypt(dataBytes, recordKey, true) - const encryptedExtra = await platform.aesCbcEncrypt(extraBytes, recordKey, true) - const encryptedRecordKey = await platform.aesCbcEncrypt(recordKey, auth.dataKey!, true) - - const cmd = recordAddCommand({ - record_uid: recordUid, - record_key: webSafe64FromBytes(encryptedRecordKey), - record_type: 'password', - folder_type: FolderKind.UserFolder, - how_long_ago: 0, - folder_uid: folderUid || '', - folder_key: '', - data: webSafe64FromBytes(encryptedData), - extra: webSafe64FromBytes(encryptedExtra), - non_shared_data: '', - file_ids: [], - }) - - const response = await auth.executeRestCommand(cmd) - - return { - recordUid, - success: response.result_code === ResultCode.Success, - status: response.result_code, - } + const recordUidBytes = generateUidBytes(); + const recordKey = generateEncryptionKey(); + const recordUid = webSafe64FromBytes(recordUidBytes); + + const recordDataJson = JSON.stringify({ + title: data.title || "", + secret1: data.login || "", + secret2: data.password || "", + link: data.url || "", + notes: data.notes || "", + custom: (data.custom || []).map((c) => ({ + name: c.name, + value: c.value, + type: c.type || "text", + })), + }); + + const extraJson = JSON.stringify(buildLegacyPasswordExtra(data, recordUid)); + + const dataBytes = new TextEncoder().encode(recordDataJson); + const extraBytes = new TextEncoder().encode(extraJson); + + const encryptedData = await platform.aesCbcEncrypt( + dataBytes, + recordKey, + true, + ); + const encryptedExtra = await platform.aesCbcEncrypt( + extraBytes, + recordKey, + true, + ); + const encryptedRecordKey = await platform.aesCbcEncrypt( + recordKey, + auth.dataKey!, + true, + ); + + const cmd = recordAddCommand({ + record_uid: recordUid, + record_key: webSafe64FromBytes(encryptedRecordKey), + record_type: "password", + folder_type: FolderKind.UserFolder, + how_long_ago: 0, + folder_uid: folderUid || "", + folder_key: "", + data: webSafe64FromBytes(encryptedData), + extra: webSafe64FromBytes(encryptedExtra), + non_shared_data: "", + file_ids: [], + }); + + const response = await auth.executeRestCommand(cmd); + + return { + recordUid, + success: response.result_code === ResultCode.Success, + status: response.result_code, + }; } async function addTypedRecord( - auth: Auth, - data: TypedRecordData, - folderUid?: string + auth: Auth, + data: TypedRecordData, + folderUid?: string, ): Promise { - const recordUidBytes = generateUidBytes() - const recordKey = generateEncryptionKey() - const recordUid = webSafe64FromBytes(recordUidBytes) - - const recordPayload = { - type: data.type, - title: data.title, - fields: data.fields || [], - custom: data.custom || [], - notes: data.notes || '', - } - - const dataBytes = getPaddedJsonBytes(recordPayload) - const encryptedData = await platform.aesGcmEncrypt(dataBytes, recordKey) - const encryptedRecordKey = await platform.aesGcmEncrypt(recordKey, auth.dataKey!) - - const recordAdd: Records.IRecordAdd = { - recordUid: recordUidBytes, - recordKey: encryptedRecordKey, - clientModifiedTime: Date.now(), - data: encryptedData, - folderType: Records.RecordFolderType.user_folder, - } - - if (folderUid) { - recordAdd.folderUid = normal64Bytes(folderUid) - } - - const request: Records.IRecordsAddRequest = { - records: [recordAdd], - clientTime: Date.now(), - } - - const msg = recordsAddMessage(request) - const response = await auth.executeRest(msg) - - const recordStatus = response.records?.[0] - const success = - recordStatus?.status === Records.RecordModifyResult.RS_SUCCESS || - !recordStatus?.status - - return { - recordUid, - success, - status: recordStatus?.status != null - ? Records.RecordModifyResult[recordStatus.status] - : ResultCode.OK, - } + const recordUidBytes = generateUidBytes(); + const recordKey = generateEncryptionKey(); + const recordUid = webSafe64FromBytes(recordUidBytes); + + const recordPayload = { + type: data.type, + title: data.title, + fields: data.fields || [], + custom: data.custom || [], + notes: data.notes || "", + }; + + const dataBytes = getPaddedJsonBytes(recordPayload); + const encryptedData = await platform.aesGcmEncrypt(dataBytes, recordKey); + const encryptedRecordKey = await platform.aesGcmEncrypt( + recordKey, + auth.dataKey!, + ); + + const recordAdd: Records.IRecordAdd = { + recordUid: recordUidBytes, + recordKey: encryptedRecordKey, + clientModifiedTime: Date.now(), + data: encryptedData, + folderType: Records.RecordFolderType.user_folder, + }; + + if (folderUid) { + recordAdd.folderUid = normal64Bytes(folderUid); + } + + const request: Records.IRecordsAddRequest = { + records: [recordAdd], + clientTime: Date.now(), + }; + + const msg = recordsAddMessage(request); + const response = await auth.executeRest(msg); + + const recordStatus = response.records?.[0]; + const success = + recordStatus?.status === Records.RecordModifyResult.RS_SUCCESS || + !recordStatus?.status; + + return { + recordUid, + success, + status: + recordStatus?.status != null + ? Records.RecordModifyResult[recordStatus.status] + : ResultCode.OK, + }; } export async function updateRecord( - auth: Auth, - recordUid: string, - data: TypedRecordData, - revision: number, - recordKey: Uint8Array + auth: Auth, + recordUid: string, + data: TypedRecordData, + revision: number, + recordKey: Uint8Array, ): Promise { - if (!data.title || !data.title.trim()) { - throw new KeeperSdkError('Record title is required.', 'missing_record_title') - } - if (!data.type?.trim()) { - throw new KeeperSdkError('Record type is required.', 'missing_record_type') - } - const recordUidBytes = normal64Bytes(recordUid) - - const recordPayload = { - type: data.type, - title: data.title, - fields: data.fields || [], - custom: data.custom || [], - notes: data.notes || '', - } - - const dataBytes = getPaddedJsonBytes(recordPayload) - const encryptedData = await platform.aesGcmEncrypt(dataBytes, recordKey) - - const recordUpdate: Records.IRecordUpdate = { - recordUid: recordUidBytes, - clientModifiedTime: Date.now(), - revision, - data: encryptedData, - } - - const request: Records.IRecordsUpdateRequest = { - records: [recordUpdate], - clientTime: Date.now(), - } - - const msg = recordsUpdateMessage(request) - const response = await auth.executeRest(msg) - - const recordStatus = response.records?.[0] - const success = - recordStatus?.status === Records.RecordModifyResult.RS_SUCCESS || - !recordStatus?.status - - return { - recordUid, - success, - status: recordStatus?.status != null - ? Records.RecordModifyResult[recordStatus.status] - : ResultCode.OK, - } + if (!data.title || !data.title.trim()) { + throw new KeeperSdkError( + "Record title is required.", + "missing_record_title", + ); + } + if (!data.type?.trim()) { + throw new KeeperSdkError("Record type is required.", "missing_record_type"); + } + const recordUidBytes = normal64Bytes(recordUid); + + const recordPayload = { + type: data.type, + title: data.title, + fields: data.fields || [], + custom: data.custom || [], + notes: data.notes || "", + }; + + const dataBytes = getPaddedJsonBytes(recordPayload); + const encryptedData = await platform.aesGcmEncrypt(dataBytes, recordKey); + + const recordUpdate: Records.IRecordUpdate = { + recordUid: recordUidBytes, + clientModifiedTime: Date.now(), + revision, + data: encryptedData, + }; + + const request: Records.IRecordsUpdateRequest = { + records: [recordUpdate], + clientTime: Date.now(), + }; + + const msg = recordsUpdateMessage(request); + const response = await auth.executeRest(msg); + + const recordStatus = response.records?.[0]; + const success = + recordStatus?.status === Records.RecordModifyResult.RS_SUCCESS || + !recordStatus?.status; + + return { + recordUid, + success, + status: + recordStatus?.status != null + ? Records.RecordModifyResult[recordStatus.status] + : ResultCode.OK, + }; } export async function deleteRecord( - auth: Auth, - recordUid: string + auth: Auth, + recordUid: string, ): Promise { - const preDeleteRequest = { - objects: [ - { - object_uid: recordUid, - object_type: VaultObjectKind.Record, - from_uid: '', - from_type: FolderKind.UserFolder, - delete_resolution: DeleteResolution.Unlink, - } as RecordPreDeleteObject, - ], - } - - let preDeleteResponse: KeeperPreDeleteResponse - try { - const preDeleteCmd = recordPreDeleteCommand(preDeleteRequest) - preDeleteResponse = await auth.executeRestCommand(preDeleteCmd) - } catch (err) { - return { recordUid, success: false, message: extractErrorMessage(err) } - } - - const token = preDeleteResponse?.pre_delete_response?.pre_delete_token - if (!token) { - return { - recordUid, - success: false, - message: preDeleteResponse?.message || preDeleteResponse?.result_code || 'pre_delete failed: no token', - } - } - - try { - const deleteCmd = recordDeleteCommand({ pre_delete_token: token }) - await auth.executeRestCommand(deleteCmd) - } catch (err) { - return { recordUid, success: false, message: extractErrorMessage(err) } - } - - return { recordUid, success: true, message: ResultCode.Success } + const preDeleteRequest = { + objects: [ + { + object_uid: recordUid, + object_type: VaultObjectKind.Record, + from_uid: "", + from_type: FolderKind.UserFolder, + delete_resolution: DeleteResolution.Unlink, + } as RecordPreDeleteObject, + ], + }; + + let preDeleteResponse: KeeperPreDeleteResponse; + try { + const preDeleteCmd = recordPreDeleteCommand(preDeleteRequest); + preDeleteResponse = await auth.executeRestCommand(preDeleteCmd); + } catch (err) { + return { recordUid, success: false, message: extractErrorMessage(err) }; + } + + const token = preDeleteResponse?.pre_delete_response?.pre_delete_token; + if (!token) { + return { + recordUid, + success: false, + message: + preDeleteResponse?.message || + preDeleteResponse?.result_code || + "pre_delete failed: no token", + }; + } + + try { + const deleteCmd = recordDeleteCommand({ pre_delete_token: token }); + await auth.executeRestCommand(deleteCmd); + } catch (err) { + return { recordUid, success: false, message: extractErrorMessage(err) }; + } + + return { recordUid, success: true, message: ResultCode.Success }; } export type HistoryEntry = { - revision: number - version: number - userName: string - clientModifiedTime: number - data: Record | null -} + revision: number; + version: number; + userName: string; + clientModifiedTime: number; + data: Record | null; +}; export type RecordHistoryResult = { - recordUid: string - history: HistoryEntry[] -} + recordUid: string; + history: HistoryEntry[]; +}; export async function getRecordHistory( - auth: Auth, - recordUid: string, - recordKey: Uint8Array + auth: Auth, + recordUid: string, + recordKey: Uint8Array, ): Promise { - const cmd = getRecordHistoryCommand({ - record_uid: recordUid, - client_time: Date.now(), - }) - - let response: GetRecordHistoryResponse - try { - response = await auth.executeRestCommand(cmd) - } catch (err) { - throw KeeperSdkError.from(err) - } - - const rawHistory = response.history || [] - const history: HistoryEntry[] = [] - - for (const entry of rawHistory) { - let decryptedData: Record | null = null - - if (entry.data) { - try { - const dataBytes = normal64Bytes(entry.data) - const version = entry.version || 0 - let decrypted: Uint8Array - - if (version <= RecordVersion.Legacy) { - decrypted = await platform.aesCbcDecrypt(dataBytes, recordKey, true) - } else { - decrypted = await platform.aesGcmDecrypt(dataBytes, recordKey) - } - - decryptedData = JSON.parse(platform.bytesToString(decrypted)) - } catch (err) { - logger.debug(`Failed to decrypt history revision ${entry.revision}:`, extractErrorMessage(err)) - decryptedData = null - } + const cmd = getRecordHistoryCommand({ + record_uid: recordUid, + client_time: Date.now(), + }); + + let response: GetRecordHistoryResponse; + try { + response = await auth.executeRestCommand(cmd); + } catch (err) { + throw KeeperSdkError.from(err); + } + + const rawHistory = response.history || []; + const history: HistoryEntry[] = []; + + for (const entry of rawHistory) { + let decryptedData: Record | null = null; + + if (entry.data) { + try { + const dataBytes = normal64Bytes(entry.data); + const version = entry.version || 0; + let decrypted: Uint8Array; + + if (version <= RecordVersion.Legacy) { + decrypted = await platform.aesCbcDecrypt(dataBytes, recordKey, true); + } else { + decrypted = await platform.aesGcmDecrypt(dataBytes, recordKey); } - history.push({ - revision: entry.revision, - version: entry.version, - userName: entry.user_name || '', - clientModifiedTime: entry.client_modified_time || 0, - data: decryptedData, - }) - } - - return { recordUid, history } + decryptedData = JSON.parse(platform.bytesToString(decrypted)); + } catch (err) { + logger.debug( + `Failed to decrypt history revision ${entry.revision}:`, + extractErrorMessage(err), + ); + decryptedData = null; + } + } + + history.push({ + revision: entry.revision, + version: entry.version, + userName: entry.user_name || "", + clientModifiedTime: entry.client_modified_time || 0, + data: decryptedData, + }); + } + + return { recordUid, history }; } export type MoveRecordInput = { - recordUid: string - dstFolderUid: string - srcFolderUid?: string - link?: boolean - canEdit?: boolean - canShare?: boolean -} + recordUid: string; + dstFolderUid: string; + srcFolderUid?: string; + link?: boolean; + canEdit?: boolean; + canShare?: boolean; +}; export type MoveRecordResult = { - recordUid: string - success: boolean - message: string -} + recordUid: string; + success: boolean; + message: string; +}; type FolderInfo = { - uid: string - folderType: FolderKind - scopeUid: string -} + uid: string; + folderType: FolderKind; + scopeUid: string; +}; function resolveFolder(uid: string, storage: InMemoryStorage): FolderInfo { - if (!uid) { - return { uid: '', folderType: FolderKind.UserFolder, scopeUid: '' } - } - - if (storage.getByUid(FolderKind.UserFolder, uid)) { - return { uid, folderType: FolderKind.UserFolder, scopeUid: '' } - } - - if (storage.getByUid(FolderKind.SharedFolder, uid)) { - return { uid, folderType: FolderKind.SharedFolder, scopeUid: uid } - } - - const sfFolder = storage.getByUid(FolderKind.SharedFolderFolder, uid) - if (sfFolder) { - return { uid, folderType: FolderKind.SharedFolderFolder, scopeUid: sfFolder.sharedFolderUid } - } + if (!uid) { + return { uid: "", folderType: FolderKind.UserFolder, scopeUid: "" }; + } + + if (storage.getByUid(FolderKind.UserFolder, uid)) { + return { uid, folderType: FolderKind.UserFolder, scopeUid: "" }; + } + + if (storage.getByUid(FolderKind.SharedFolder, uid)) { + return { uid, folderType: FolderKind.SharedFolder, scopeUid: uid }; + } + + const sfFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + uid, + ); + if (sfFolder) { + return { + uid, + folderType: FolderKind.SharedFolderFolder, + scopeUid: sfFolder.sharedFolderUid, + }; + } - return { uid, folderType: FolderKind.UserFolder, scopeUid: '' } + return { uid, folderType: FolderKind.UserFolder, scopeUid: "" }; } -async function findRecordSourceFolder(recordUid: string, storage: InMemoryStorage): Promise { - const folderKinds = [ - FolderKind.UserFolder, - FolderKind.SharedFolder, - FolderKind.SharedFolderFolder, - ] as const - - for (const kind of folderKinds) { - for (const folder of storage.getAll(kind)) { - const dependencies = (await storage.getDependencies(folder.uid)) || [] - if ( - dependencies.some( - (dependency) => dependency.kind === VaultObjectKind.Record && dependency.uid === recordUid - ) - ) { - return folder.uid - } - } - } - - const sharedFolderRecord = storage - .getAll(VaultObjectKind.SharedFolderRecord) - .find((candidate) => candidate.recordUid === recordUid) - return sharedFolderRecord ? sharedFolderRecord.sharedFolderUid : '' +async function findRecordSourceFolder( + recordUid: string, + storage: InMemoryStorage, +): Promise { + const folderKinds = [ + FolderKind.UserFolder, + FolderKind.SharedFolder, + FolderKind.SharedFolderFolder, + ] as const; + + for (const kind of folderKinds) { + for (const folder of storage.getAll< + DUserFolder | DSharedFolder | DSharedFolderFolder + >(kind)) { + const dependencies = (await storage.getDependencies(folder.uid)) || []; + if ( + dependencies.some( + (dependency) => + dependency.kind === VaultObjectKind.Record && + dependency.uid === recordUid, + ) + ) { + return folder.uid; + } + } + } + + const sharedFolderRecord = storage + .getAll(VaultObjectKind.SharedFolderRecord) + .find((candidate) => candidate.recordUid === recordUid); + return sharedFolderRecord ? sharedFolderRecord.sharedFolderUid : ""; } export async function moveRecord( - auth: Auth, - storage: InMemoryStorage, - input: MoveRecordInput + auth: Auth, + storage: InMemoryStorage, + input: MoveRecordInput, ): Promise { - const { - recordUid, - dstFolderUid, - link = false, - canEdit, - canShare, - } = input - - const dst = resolveFolder(dstFolderUid, storage) - - const srcUid = - input.srcFolderUid !== undefined ? input.srcFolderUid : await findRecordSourceFolder(recordUid, storage) - const src = resolveFolder(srcUid, storage) - - const moveObj: MoveObject = { - uid: recordUid, - type: VaultObjectKind.Record, - cascade: false, - from_type: src.folderType, - from_uid: src.uid || undefined, - can_edit: canEdit, - can_reshare: canShare, - } + const { recordUid, dstFolderUid, link = false, canEdit, canShare } = input; - const transitionKeys: TransitionKeyObject[] = [] + const dst = resolveFolder(dstFolderUid, storage); - if (src.scopeUid !== dst.scopeUid) { - const recordKey = await storage.getKeyBytes(recordUid) - if (!recordKey) { - return { recordUid, success: false, message: 'Record key not found' } - } - - let dstKey: Uint8Array | undefined - if (dst.scopeUid) { - dstKey = await storage.getKeyBytes(dst.scopeUid) - } else { - dstKey = auth.dataKey - } + const srcUid = + input.srcFolderUid !== undefined + ? input.srcFolderUid + : await findRecordSourceFolder(recordUid, storage); + const src = resolveFolder(srcUid, storage); - if (!dstKey) { - return { recordUid, success: false, message: 'Destination folder key not found' } - } + const moveObj: MoveObject = { + uid: recordUid, + type: VaultObjectKind.Record, + cascade: false, + from_type: src.folderType, + from_uid: src.uid || undefined, + can_edit: canEdit, + can_reshare: canShare, + }; - const record = storage.getByUid(VaultObjectKind.Record, recordUid) - const version = record?.version || RecordVersion.Typed + const transitionKeys: TransitionKeyObject[] = []; - let encryptedKey: Uint8Array - if (version >= RecordVersion.Typed) { - encryptedKey = await platform.aesGcmEncrypt(recordKey, dstKey) - } else { - encryptedKey = await platform.aesCbcEncrypt(recordKey, dstKey, true) - } - - transitionKeys.push({ uid: recordUid, key: webSafe64FromBytes(encryptedKey) }) + if (src.scopeUid !== dst.scopeUid) { + const recordKey = await storage.getKeyBytes(recordUid); + if (!recordKey) { + return { recordUid, success: false, message: "Record key not found" }; } - const request: MoveRequest = { - to_type: dst.folderType, - to_uid: dst.uid || undefined, - link, - move: [moveObj], - transition_keys: transitionKeys, + let dstKey: Uint8Array | undefined; + if (dst.scopeUid) { + dstKey = await storage.getKeyBytes(dst.scopeUid); + } else { + dstKey = auth.dataKey; } - try { - const cmd = moveCommand(request) - await auth.executeRestCommand(cmd) - } catch (err) { - return { recordUid, success: false, message: extractErrorMessage(err) } - } - - return { recordUid, success: true, message: 'Record moved successfully' } + if (!dstKey) { + return { + recordUid, + success: false, + message: "Destination folder key not found", + }; + } + + const record = storage.getByUid(VaultObjectKind.Record, recordUid); + const version = record?.version || RecordVersion.Typed; + + let encryptedKey: Uint8Array; + if (version >= RecordVersion.Typed) { + encryptedKey = await platform.aesGcmEncrypt(recordKey, dstKey); + } else { + encryptedKey = await platform.aesCbcEncrypt(recordKey, dstKey, true); + } + + transitionKeys.push({ + uid: recordUid, + key: webSafe64FromBytes(encryptedKey), + }); + } + + const request: MoveRequest = { + to_type: dst.folderType, + to_uid: dst.uid || undefined, + link, + move: [moveObj], + transition_keys: transitionKeys, + }; + + try { + const cmd = moveCommand(request); + await auth.executeRestCommand(cmd); + } catch (err) { + return { recordUid, success: false, message: extractErrorMessage(err) }; + } + + return { recordUid, success: true, message: "Record moved successfully" }; } diff --git a/KeeperSdk/src/records/RecordUtils.ts b/KeeperSdk/src/records/RecordUtils.ts index 699f5f8c..9820acbe 100644 --- a/KeeperSdk/src/records/RecordUtils.ts +++ b/KeeperSdk/src/records/RecordUtils.ts @@ -1,360 +1,384 @@ -import type { DRecord } from '@keeper-security/keeperapi' -import { getTotpCode } from './Totp' +import type { DRecord } from "@keeper-security/keeperapi"; +import { getTotpCode } from "./Totp"; enum FieldType { - Login = 'login', - Password = 'password', - Url = 'url', - Note = 'note', - Text = 'text', + Login = "login", + Password = "password", + Url = "url", + Note = "note", + Text = "text", } export enum RecordVersion { - Legacy = 2, - Typed = 3, + Legacy = 2, + Typed = 3, } -const TOTP_FIELD_TYPES = new Set(['totp', 'oneTimeCode', 'otp']) -const MASKED_VALUE = '********' -const RECORD_SEPARATOR = '-'.repeat(50) +const TOTP_FIELD_TYPES = new Set(["totp", "oneTimeCode", "otp"]); +const MASKED_VALUE = "********"; +const RECORD_SEPARATOR = "-".repeat(50); type RecordField = { - type: string - value: any[] - label?: string - required?: boolean - privacyScreen?: boolean - enforceGeneration?: boolean - complexity?: { - length?: number - caps?: number - lowercase?: number - digits?: number - special?: number - } -} + type: string; + value: any[]; + label?: string; + required?: boolean; + privacyScreen?: boolean; + enforceGeneration?: boolean; + complexity?: { + length?: number; + caps?: number; + lowercase?: number; + digits?: number; + special?: number; + }; +}; type LegacyExtraField = { - type?: string - field_type?: string - value?: unknown - data?: unknown - label?: string - field_title?: string -} + type?: string; + field_type?: string; + value?: unknown; + data?: unknown; + label?: string; + field_title?: string; +}; function toFieldValueArray(v: unknown): any[] { - if (v == null) return [] - return Array.isArray(v) ? v : [v] + if (v == null) return []; + return Array.isArray(v) ? v : [v]; } function getLegacyExtraFields(record: DRecord): RecordField[] { - const raw = record.extra - if (raw == null) return [] - let extra: { fields?: LegacyExtraField[] } - if (typeof raw === 'string') { - try { - extra = JSON.parse(raw) - } catch { - return [] - } - } else if (typeof raw === 'object') { - extra = raw - } else { - return [] - } - if (!Array.isArray(extra.fields)) return [] - const out: RecordField[] = [] - for (const f of extra.fields) { - const typeName = f.type || f.field_type || FieldType.Text - const rawVal = f.value !== undefined && f.value !== null ? f.value : f.data - out.push({ - type: typeName, - value: toFieldValueArray(rawVal), - label: f.label || f.field_title, - }) + const raw = record.extra; + if (raw == null) return []; + let extra: { fields?: LegacyExtraField[] }; + if (typeof raw === "string") { + try { + extra = JSON.parse(raw); + } catch { + return []; } - return out + } else if (typeof raw === "object") { + extra = raw; + } else { + return []; + } + if (!Array.isArray(extra.fields)) return []; + const out: RecordField[] = []; + for (const f of extra.fields) { + const typeName = f.type || f.field_type || FieldType.Text; + const rawVal = f.value !== undefined && f.value !== null ? f.value : f.data; + out.push({ + type: typeName, + value: toFieldValueArray(rawVal), + label: f.label || f.field_title, + }); + } + return out; } export function getRecordTitle(record: DRecord): string { - if (!record.data) return '(no data)' - if (typeof record.data === 'string') { - try { - const parsed = JSON.parse(record.data) - return parsed.title || '(untitled)' - } catch { - return '(parse error)' - } + if (!record.data) return "(no data)"; + if (typeof record.data === "string") { + try { + const parsed = JSON.parse(record.data); + return parsed.title || "(untitled)"; + } catch { + return "(parse error)"; } - return record.data.title || '(untitled)' + } + return record.data.title || "(untitled)"; } export function getRecordType(record: DRecord): string { - if (record.version <= RecordVersion.Legacy) return 'legacy' - if (!record.data) return 'unknown' - return record.data.type || 'unknown' + if (record.version <= RecordVersion.Legacy) return "legacy"; + if (!record.data) return "unknown"; + return record.data.type || "unknown"; } export function getRecordFields(record: DRecord): RecordField[] { - if (!record.data) return [] - - if (record.version <= RecordVersion.Legacy) { - const fields: RecordField[] = [] - const d = record.data - if (d.secret1) fields.push({ type: FieldType.Login, value: [d.secret1] }) - if (d.secret2) fields.push({ type: FieldType.Password, value: [d.secret2] }) - if (d.link) fields.push({ type: FieldType.Url, value: [d.link] }) - if (d.notes) fields.push({ type: FieldType.Note, value: [d.notes] }) - if (Array.isArray(d.custom)) { - for (const c of d.custom) { - if (!c) continue - fields.push({ - type: c.type || FieldType.Text, - value: c.value != null && c.value !== '' ? [c.value] : [], - label: c.name, - }) - } - } - const extraFields = getLegacyExtraFields(record) - fields.push(...extraFields) - return fields + if (!record.data) return []; + + if (record.version <= RecordVersion.Legacy) { + const fields: RecordField[] = []; + const d = record.data; + if (d.secret1) fields.push({ type: FieldType.Login, value: [d.secret1] }); + if (d.secret2) + fields.push({ type: FieldType.Password, value: [d.secret2] }); + if (d.link) fields.push({ type: FieldType.Url, value: [d.link] }); + if (d.notes) fields.push({ type: FieldType.Note, value: [d.notes] }); + if (Array.isArray(d.custom)) { + for (const c of d.custom) { + if (!c) continue; + fields.push({ + type: c.type || FieldType.Text, + value: c.value != null && c.value !== "" ? [c.value] : [], + label: c.name, + }); + } } - - const fields: RecordField[] = [] - if (Array.isArray(record.data.fields)) { - for (const f of record.data.fields) { - fields.push({ - type: f.type || FieldType.Text, - value: Array.isArray(f.value) ? f.value : [f.value], - label: f.label, - required: f.required, - privacyScreen: f.privacyScreen, - enforceGeneration: f.enforceGeneration, - complexity: f.complexity, - }) - } + const extraFields = getLegacyExtraFields(record); + fields.push(...extraFields); + return fields; + } + + const fields: RecordField[] = []; + if (Array.isArray(record.data.fields)) { + for (const f of record.data.fields) { + fields.push({ + type: f.type || FieldType.Text, + value: Array.isArray(f.value) ? f.value : [f.value], + label: f.label, + required: f.required, + privacyScreen: f.privacyScreen, + enforceGeneration: f.enforceGeneration, + complexity: f.complexity, + }); } - if (Array.isArray(record.data.custom)) { - for (const f of record.data.custom) { - fields.push({ - type: f.type || FieldType.Text, - value: Array.isArray(f.value) ? f.value : [f.value], - label: f.label, - required: f.required, - privacyScreen: f.privacyScreen, - enforceGeneration: f.enforceGeneration, - complexity: f.complexity, - }) - } + } + if (Array.isArray(record.data.custom)) { + for (const f of record.data.custom) { + fields.push({ + type: f.type || FieldType.Text, + value: Array.isArray(f.value) ? f.value : [f.value], + label: f.label, + required: f.required, + privacyScreen: f.privacyScreen, + enforceGeneration: f.enforceGeneration, + complexity: f.complexity, + }); } - return fields + } + return fields; } export type RecordSummary = { - login?: string - password?: string - url?: string - fields: RecordField[] -} + login?: string; + password?: string; + url?: string; + fields: RecordField[]; +}; export function getRecordSummary(record: DRecord): RecordSummary { - const fields = getRecordFields(record) - if (record.version <= RecordVersion.Legacy) { - return { - login: record.data?.secret1, - password: record.data?.secret2, - url: record.data?.link, - fields, - } - } - - let login: string | undefined - let password: string | undefined - let url: string | undefined - - for (const f of fields) { - if (!login && f.type === FieldType.Login && f.value.length > 0) { - login = String(f.value[0]) - } else if (!password && f.type === FieldType.Password && f.value.length > 0) { - password = String(f.value[0]) - } else if (!url && f.type === FieldType.Url && f.value.length > 0) { - const val = f.value[0] - url = typeof val === 'string' ? val : val?.value || val?.url - } + const fields = getRecordFields(record); + if (record.version <= RecordVersion.Legacy) { + return { + login: record.data?.secret1, + password: record.data?.secret2, + url: record.data?.link, + fields, + }; + } + + let login: string | undefined; + let password: string | undefined; + let url: string | undefined; + + for (const f of fields) { + if (!login && f.type === FieldType.Login && f.value.length > 0) { + login = String(f.value[0]); + } else if ( + !password && + f.type === FieldType.Password && + f.value.length > 0 + ) { + password = String(f.value[0]); + } else if (!url && f.type === FieldType.Url && f.value.length > 0) { + const val = f.value[0]; + url = typeof val === "string" ? val : val?.value || val?.url; } + } - return { login, password, url, fields } + return { login, password, url, fields }; } export function getRecordTotpUrl(record: DRecord): string | undefined { - for (const field of getRecordFields(record)) { - if (!TOTP_FIELD_TYPES.has(field.type)) continue - for (const v of field.value) { - if (typeof v === 'string' && v.trim()) return v.trim() - } + for (const field of getRecordFields(record)) { + if (!TOTP_FIELD_TYPES.has(field.type)) continue; + for (const v of field.value) { + if (typeof v === "string" && v.trim()) return v.trim(); } - return undefined + } + return undefined; } export function getRecordPassword(record: DRecord): string | undefined { - return getRecordSummary(record).password + return getRecordSummary(record).password; } export function getRecordLogin(record: DRecord): string | undefined { - return getRecordSummary(record).login + return getRecordSummary(record).login; } export function getRecordUrl(record: DRecord): string | undefined { - return getRecordSummary(record).url + return getRecordSummary(record).url; } /** Commander-style list description (login @ url for most record types). */ export function getRecordDescription(record: DRecord): string { - if (record.version === 6) return 'PAM Configuration' + if (record.version === 6) return "PAM Configuration"; - const summary = getRecordSummary(record) - const parts: string[] = [] - if (summary.login) parts.push(summary.login) - if (summary.url) parts.push(summary.url) - return parts.length > 0 ? parts.join(' @ ') : '' + const summary = getRecordSummary(record); + const parts: string[] = []; + if (summary.login) parts.push(summary.login); + if (summary.url) parts.push(summary.url); + return parts.length > 0 ? parts.join(" @ ") : ""; } /** Commander list column: Nested Share / Keeper Drive vs classic vault records. */ -export function getRecordCategory(record: DRecord): 'Classic' | 'Nested' { - return record.isKeeperDriveData ? 'Nested' : 'Classic' +export function getRecordCategory(record: DRecord): "Classic" | "Nested" { + return record.isKeeperDriveData ? "Nested" : "Classic"; } -const wordCache = new WeakMap() +const wordCache = new WeakMap(); export function searchRecords(records: DRecord[], criteria: string): DRecord[] { - if (!criteria.trim()) return records + if (!criteria.trim()) return records; - const searchWords = criteria.toLowerCase().split(/\s+/) + const searchWords = criteria.toLowerCase().split(/\s+/); - return records.filter((record) => { - let words = wordCache.get(record) - if (!words) { - words = collectRecordWords(record) - wordCache.set(record, words) - } - return searchWords.every((sw) => words!.some((w) => w.includes(sw))) - }) + return records.filter((record) => { + let words = wordCache.get(record); + if (!words) { + words = collectRecordWords(record); + wordCache.set(record, words); + } + return searchWords.every((sw) => words!.some((w) => w.includes(sw))); + }); } function collectRecordWords(record: DRecord): string[] { - const words: string[] = [] - const title = getRecordTitle(record) - if (title) words.push(...title.toLowerCase().split(/\s+/)) - - for (const field of getRecordFields(record)) { - if (field.label) words.push(field.label.toLowerCase()) - for (const v of field.value) { - if (typeof v === 'string') { - words.push(...v.toLowerCase().split(/\s+/)) - } else if (v && typeof v === 'object') { - for (const val of Object.values(v)) { - if (typeof val === 'string') { - words.push(...val.toLowerCase().split(/\s+/)) - } - } - } + const words: string[] = []; + const title = getRecordTitle(record); + if (title) words.push(...title.toLowerCase().split(/\s+/)); + + for (const field of getRecordFields(record)) { + if (field.label) words.push(field.label.toLowerCase()); + for (const v of field.value) { + if (typeof v === "string") { + words.push(...v.toLowerCase().split(/\s+/)); + } else if (v && typeof v === "object") { + for (const val of Object.values(v)) { + if (typeof val === "string") { + words.push(...val.toLowerCase().split(/\s+/)); + } } + } } + } - words.push(record.uid) - return words + words.push(record.uid); + return words; } export type FormatRecordOptions = { - showDetails?: boolean - unmask?: boolean -} - -function resolveFormatRecordOptions(showDetailsOrOptions?: boolean | FormatRecordOptions): { - showDetails: boolean - unmask: boolean + showDetails?: boolean; + unmask?: boolean; +}; + +function resolveFormatRecordOptions( + showDetailsOrOptions?: boolean | FormatRecordOptions, +): { + showDetails: boolean; + unmask: boolean; } { - if (typeof showDetailsOrOptions === 'boolean') { - return { showDetails: showDetailsOrOptions, unmask: false } - } - return { - showDetails: showDetailsOrOptions?.showDetails ?? false, - unmask: showDetailsOrOptions?.unmask ?? false, - } + if (typeof showDetailsOrOptions === "boolean") { + return { showDetails: showDetailsOrOptions, unmask: false }; + } + return { + showDetails: showDetailsOrOptions?.showDetails ?? false, + unmask: showDetailsOrOptions?.unmask ?? false, + }; } function formatFieldValue(field: RecordField, unmask: boolean): string { - const isTotp = TOTP_FIELD_TYPES.has(field.type) - const isSensitive = field.type === FieldType.Password || isTotp || field.privacyScreen === true - if (!isSensitive || unmask) { - return field.value.map((v) => (typeof v === 'string' ? v : JSON.stringify(v))).join(', ') - } - return MASKED_VALUE + const isTotp = TOTP_FIELD_TYPES.has(field.type); + const isSensitive = + field.type === FieldType.Password || isTotp || field.privacyScreen === true; + if (!isSensitive || unmask) { + return field.value + .map((v) => (typeof v === "string" ? v : JSON.stringify(v))) + .join(", "); + } + return MASKED_VALUE; } -export function formatRecordFields(record: DRecord, unmask: boolean): { name: string; value: unknown }[] { - const summary = getRecordSummary(record) - const fields: { name: string; value: unknown }[] = [ - { name: 'title', value: getRecordTitle(record) }, - { name: 'record_uid', value: record.uid }, - { name: 'version', value: record.version }, - { name: 'record_type', value: getRecordType(record) }, - ] - if (summary.login) fields.push({ name: 'login', value: summary.login }) - if (summary.password) { - fields.push({ - name: 'password', - value: unmask ? summary.password : MASKED_VALUE, - }) - } - if (summary.url) fields.push({ name: 'login_url', value: summary.url }) - for (const field of summary.fields) { - if (field.type === FieldType.Login || field.type === FieldType.Url) continue - const label = TOTP_FIELD_TYPES.has(field.type) - ? 'TOTP URL' - : (field.label || field.type).replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - fields.push({ name: label, value: formatFieldValue(field, unmask) }) - } - const notes = record.version <= RecordVersion.Legacy ? record.data?.notes : undefined - if (notes) fields.push({ name: 'Notes', value: notes }) - return fields +export function formatRecordFields( + record: DRecord, + unmask: boolean, +): { name: string; value: unknown }[] { + const summary = getRecordSummary(record); + const fields: { name: string; value: unknown }[] = [ + { name: "title", value: getRecordTitle(record) }, + { name: "record_uid", value: record.uid }, + { name: "version", value: record.version }, + { name: "record_type", value: getRecordType(record) }, + ]; + if (summary.login) fields.push({ name: "login", value: summary.login }); + if (summary.password) { + fields.push({ + name: "password", + value: unmask ? summary.password : MASKED_VALUE, + }); + } + if (summary.url) fields.push({ name: "login_url", value: summary.url }); + for (const field of summary.fields) { + if (field.type === FieldType.Login || field.type === FieldType.Url) + continue; + const label = TOTP_FIELD_TYPES.has(field.type) + ? "TOTP URL" + : (field.label || field.type) + .replace(/_/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); + fields.push({ name: label, value: formatFieldValue(field, unmask) }); + } + const notes = + record.version <= RecordVersion.Legacy ? record.data?.notes : undefined; + if (notes) fields.push({ name: "Notes", value: notes }); + return fields; } -export function formatRecord(record: DRecord, showDetailsOrOptions?: boolean | FormatRecordOptions): string { - const { showDetails, unmask } = resolveFormatRecordOptions(showDetailsOrOptions) - const summary = getRecordSummary(record) - const lines: string[] = [ - RECORD_SEPARATOR, - `Title: ${getRecordTitle(record)}`, - `Record UID: ${record.uid}`, - `Record Type: ${getRecordType(record)}`, - ] - - if (summary.login) lines.push(`Username: ${summary.login}`) - if (summary.url) lines.push(`URL: ${summary.url}`) - if (summary.password) { - lines.push(`Password: ${unmask ? summary.password : MASKED_VALUE}`) +export function formatRecord( + record: DRecord, + showDetailsOrOptions?: boolean | FormatRecordOptions, +): string { + const { showDetails, unmask } = + resolveFormatRecordOptions(showDetailsOrOptions); + const summary = getRecordSummary(record); + const lines: string[] = [ + RECORD_SEPARATOR, + `Title: ${getRecordTitle(record)}`, + `Record UID: ${record.uid}`, + `Record Type: ${getRecordType(record)}`, + ]; + + if (summary.login) lines.push(`Username: ${summary.login}`); + if (summary.url) lines.push(`URL: ${summary.url}`); + if (summary.password) { + lines.push(`Password: ${unmask ? summary.password : MASKED_VALUE}`); + } + + if (showDetails) { + for (const field of summary.fields) { + if (field.type === FieldType.Login || field.type === FieldType.Url) + continue; + if (field.type === FieldType.Password) continue; + const isTotp = TOTP_FIELD_TYPES.has(field.type); + const label = isTotp ? "TOTP URL" : field.label || field.type; + lines.push(`${label}: ${formatFieldValue(field, unmask)}`); } - if (showDetails) { - for (const field of summary.fields) { - if (field.type === FieldType.Login || field.type === FieldType.Url) continue - if (field.type === FieldType.Password) continue - const isTotp = TOTP_FIELD_TYPES.has(field.type) - const label = isTotp ? 'TOTP URL' : field.label || field.type - lines.push(`${label}: ${formatFieldValue(field, unmask)}`) - } - - const totpUrl = getRecordTotpUrl(record) - if (unmask && totpUrl) { - lines.push(`TOTP URL: ${totpUrl}`) - } - const code = totpUrl ? getTotpCode(totpUrl) : null - if (code) { - lines.push(`Two Factor Code: ${code.code} valid for ${code.secondsRemaining} sec`) - } + const totpUrl = getRecordTotpUrl(record); + if (unmask && totpUrl) { + lines.push(`TOTP URL: ${totpUrl}`); + } + const code = totpUrl ? getTotpCode(totpUrl) : null; + if (code) { + lines.push( + `Two Factor Code: ${code.code} valid for ${code.secondsRemaining} sec`, + ); } + } - return lines.join('\n') + return lines.join("\n"); } diff --git a/KeeperSdk/src/records/Totp.ts b/KeeperSdk/src/records/Totp.ts index 92980757..acd9c99a 100644 --- a/KeeperSdk/src/records/Totp.ts +++ b/KeeperSdk/src/records/Totp.ts @@ -1,116 +1,123 @@ -import { getSdkPlatform } from '../platform' +import { getSdkPlatform } from "../platform"; -export type TotpAlgorithm = 'SHA1' | 'SHA256' | 'SHA512' +export type TotpAlgorithm = "SHA1" | "SHA256" | "SHA512"; export type TotpParams = { - secret: string - algorithm: TotpAlgorithm - digits: number - period: number -} + secret: string; + algorithm: TotpAlgorithm; + digits: number; + period: number; +}; export type TotpCode = { - code: string - secondsRemaining: number - period: number -} + code: string; + secondsRemaining: number; + period: number; +}; -const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' -const DEFAULT_DIGITS = 6 -const DEFAULT_PERIOD = 30 -const DEFAULT_ALGORITHM: TotpAlgorithm = 'SHA1' -const UINT32_MAX = 0x100000000 +const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +const DEFAULT_DIGITS = 6; +const DEFAULT_PERIOD = 30; +const DEFAULT_ALGORITHM: TotpAlgorithm = "SHA1"; +const UINT32_MAX = 0x100000000; function decodeBase32(input: string): Uint8Array { - const cleaned = input.replace(/=+$/g, '').replace(/\s+/g, '').toUpperCase() - const out: number[] = [] - let buffer = 0 - let bits = 0 - for (const ch of cleaned) { - const idx = BASE32_ALPHABET.indexOf(ch) - if (idx < 0) throw new Error(`Invalid base32 character "${ch}" in TOTP secret`) - buffer = (buffer << 5) | idx - bits += 5 - if (bits >= 8) { - bits -= 8 - out.push((buffer >> bits) & 0xff) - } + const cleaned = input.replace(/=+$/g, "").replace(/\s+/g, "").toUpperCase(); + const out: number[] = []; + let buffer = 0; + let bits = 0; + for (const ch of cleaned) { + const idx = BASE32_ALPHABET.indexOf(ch); + if (idx < 0) + throw new Error(`Invalid base32 character "${ch}" in TOTP secret`); + buffer = (buffer << 5) | idx; + bits += 5; + if (bits >= 8) { + bits -= 8; + out.push((buffer >> bits) & 0xff); } - return Uint8Array.from(out) + } + return Uint8Array.from(out); } function normalizeAlgorithm(value: string | null | undefined): TotpAlgorithm { - const upper = (value || DEFAULT_ALGORITHM).toUpperCase() - return upper === 'SHA256' || upper === 'SHA512' ? upper : DEFAULT_ALGORITHM + const upper = (value || DEFAULT_ALGORITHM).toUpperCase(); + return upper === "SHA256" || upper === "SHA512" ? upper : DEFAULT_ALGORITHM; } function parsePositiveInt(value: string | null, fallback: number): number { - const parsed = parseInt(value || '', 10) - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback + const parsed = parseInt(value || "", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } export function parseTotpUrl(url: string): TotpParams | null { - if (!url?.trim()) return null - let params: URLSearchParams - try { - if (url.startsWith('otpauth://')) { - const u = new URL(url) - if (u.hostname.toLowerCase() !== 'totp') return null - params = u.searchParams - } else { - params = new URLSearchParams(url) - } - } catch { - return null - } - const secret = (params.get('secret') || '').trim() - if (!secret) return null - return { - secret, - algorithm: normalizeAlgorithm(params.get('algorithm')), - digits: parsePositiveInt(params.get('digits'), DEFAULT_DIGITS), - period: parsePositiveInt(params.get('period'), DEFAULT_PERIOD), + if (!url?.trim()) return null; + let params: URLSearchParams; + try { + if (url.startsWith("otpauth://")) { + const u = new URL(url); + if (u.hostname.toLowerCase() !== "totp") return null; + params = u.searchParams; + } else { + params = new URLSearchParams(url); } + } catch { + return null; + } + const secret = (params.get("secret") || "").trim(); + if (!secret) return null; + return { + secret, + algorithm: normalizeAlgorithm(params.get("algorithm")), + digits: parsePositiveInt(params.get("digits"), DEFAULT_DIGITS), + period: parsePositiveInt(params.get("period"), DEFAULT_PERIOD), + }; } function counterToBuffer(counter: number): Uint8Array { - const buf = new Uint8Array(8) - const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength) - view.setUint32(0, Math.floor(counter / UINT32_MAX), false) - view.setUint32(4, counter >>> 0, false) - return buf + const buf = new Uint8Array(8); + const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + view.setUint32(0, Math.floor(counter / UINT32_MAX), false); + view.setUint32(4, counter >>> 0, false); + return buf; } -export function getTotpCode(urlOrParams: string | TotpParams, now: number = Date.now()): TotpCode | null { - const params = typeof urlOrParams === 'string' ? parseTotpUrl(urlOrParams) : urlOrParams - if (!params) return null - if (!Number.isFinite(params.period) || params.period <= 0) return null - if (!Number.isFinite(params.digits) || params.digits <= 0) return null +export function getTotpCode( + urlOrParams: string | TotpParams, + now: number = Date.now(), +): TotpCode | null { + const params = + typeof urlOrParams === "string" ? parseTotpUrl(urlOrParams) : urlOrParams; + if (!params) return null; + if (!Number.isFinite(params.period) || params.period <= 0) return null; + if (!Number.isFinite(params.digits) || params.digits <= 0) return null; - let key: Uint8Array - try { - key = decodeBase32(params.secret) - } catch { - return null - } - if (key.length === 0) return null + let key: Uint8Array; + try { + key = decodeBase32(params.secret); + } catch { + return null; + } + if (key.length === 0) return null; - const seconds = Math.floor(now / 1000) - const counter = Math.floor(seconds / params.period) - const secondsRemaining = params.period - (seconds % params.period) + const seconds = Math.floor(now / 1000); + const counter = Math.floor(seconds / params.period); + const secondsRemaining = params.period - (seconds % params.period); - const algo = params.algorithm.toLowerCase() as 'sha1' | 'sha256' | 'sha512' - const digest = getSdkPlatform().hmac(algo, key, counterToBuffer(counter)) + const algo = params.algorithm.toLowerCase() as "sha1" | "sha256" | "sha512"; + const digest = getSdkPlatform().hmac(algo, key, counterToBuffer(counter)); - if (digest.length === 0) return null - const offset = digest[digest.length - 1] & 0x0f - if (offset + 3 >= digest.length) return null - const binary = - ((digest[offset] & 0x7f) << 24) | - ((digest[offset + 1] & 0xff) << 16) | - ((digest[offset + 2] & 0xff) << 8) | - (digest[offset + 3] & 0xff) + if (digest.length === 0) return null; + const offset = digest[digest.length - 1] & 0x0f; + if (offset + 3 >= digest.length) return null; + const binary = + ((digest[offset] & 0x7f) << 24) | + ((digest[offset + 1] & 0xff) << 16) | + ((digest[offset + 2] & 0xff) << 8) | + (digest[offset + 3] & 0xff); - const code = (binary % 10 ** params.digits).toString().padStart(params.digits, '0') - return { code, secondsRemaining, period: params.period } + const code = (binary % 10 ** params.digits) + .toString() + .padStart(params.digits, "0"); + return { code, secondsRemaining, period: params.period }; } diff --git a/KeeperSdk/src/records/listRecordsTable.ts b/KeeperSdk/src/records/listRecordsTable.ts index 17977855..866e5196 100644 --- a/KeeperSdk/src/records/listRecordsTable.ts +++ b/KeeperSdk/src/records/listRecordsTable.ts @@ -1,95 +1,103 @@ -import type { DRecord } from '@keeper-security/keeperapi' +import type { DRecord } from "@keeper-security/keeperapi"; import { - getRecordCategory, - getRecordDescription, - getRecordTitle, - getRecordType, -} from './RecordUtils' + getRecordCategory, + getRecordDescription, + getRecordTitle, + getRecordType, +} from "./RecordUtils"; -const DEFAULT_COLUMN_WIDTH = 40 -const MIN_TRUNCATE_PREFIX = 3 +const DEFAULT_COLUMN_WIDTH = 40; +const MIN_TRUNCATE_PREFIX = 3; export type FormattedRecordsListTable = { - headers: string[] - rows: string[][] -} + headers: string[]; + rows: string[][]; +}; function truncateText(text: string, maxLength: number | null): string { - if (!text) return '' - if (maxLength == null || text.length <= maxLength) return text - if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength) - return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...` + if (!text) return ""; + if (maxLength == null || text.length <= maxLength) return text; + if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength); + return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...`; } function compareByTitle(recordA: DRecord, recordB: DRecord): number { - const titleA = getRecordTitle(recordA) - const titleB = getRecordTitle(recordB) - return titleA.localeCompare(titleB, undefined, { sensitivity: 'base' }) + const titleA = getRecordTitle(recordA); + const titleB = getRecordTitle(recordB); + return titleA.localeCompare(titleB, undefined, { sensitivity: "base" }); } export function formatRecordsListTable( - records: DRecord[], - options: { verbose?: boolean; columnWidth?: number } = {} + records: DRecord[], + options: { verbose?: boolean; columnWidth?: number } = {}, ): FormattedRecordsListTable { - const { verbose = false, columnWidth = DEFAULT_COLUMN_WIDTH } = options - const maxWidth = verbose ? null : columnWidth - const sorted = [...records].sort(compareByTitle) - const headers = [ - '#', - 'Record uid', - 'Type', - 'Title', - 'Description', - 'Shared', - 'Record category', - ] - const rows = sorted.map((record, index) => { - const uid = truncateText(record.uid || '(unknown uid)', maxWidth) - const type = truncateText(getRecordType(record), maxWidth) - const title = truncateText(getRecordTitle(record), maxWidth) - const description = truncateText(getRecordDescription(record), maxWidth) - const shared = record.shared ? 'True' : 'False' - const category = getRecordCategory(record) - return [String(index + 1), uid, type, title, description, shared, category] - }) - return { headers, rows } + const { verbose = false, columnWidth = DEFAULT_COLUMN_WIDTH } = options; + const maxWidth = verbose ? null : columnWidth; + const sorted = [...records].sort(compareByTitle); + const headers = [ + "#", + "Record uid", + "Type", + "Title", + "Description", + "Shared", + "Record category", + ]; + const rows = sorted.map((record, index) => { + const uid = truncateText(record.uid || "(unknown uid)", maxWidth); + const type = truncateText(getRecordType(record), maxWidth); + const title = truncateText(getRecordTitle(record), maxWidth); + const description = truncateText(getRecordDescription(record), maxWidth); + const shared = record.shared ? "True" : "False"; + const category = getRecordCategory(record); + return [String(index + 1), uid, type, title, description, shared, category]; + }); + return { headers, rows }; } export function renderRecordsListAsciiTable( - table: FormattedRecordsListTable, - options: { minColWidth?: number } = {} + table: FormattedRecordsListTable, + options: { minColWidth?: number } = {}, ): string { - const { minColWidth = 2 } = options - const { headers, rows } = table - const columnCount = headers.length - const columnWidths: number[] = new Array(columnCount).fill(0) + const { minColWidth = 2 } = options; + const { headers, rows } = table; + const columnCount = headers.length; + const columnWidths: number[] = new Array(columnCount).fill(0); + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { + columnWidths[columnIndex] = Math.max( + headers[columnIndex].length, + minColWidth, + ); + } + for (const row of rows) { for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { - columnWidths[columnIndex] = Math.max(headers[columnIndex].length, minColWidth) - } - for (const row of rows) { - for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { - const cell = row[columnIndex] || '' - columnWidths[columnIndex] = Math.max(columnWidths[columnIndex], cell.length, minColWidth) - } - } - const padCell = (cell: string, columnIndex: number) => - cell + ' '.repeat(columnWidths[columnIndex] - cell.length) - const formatRow = (cells: string[]) => cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - const ruleRow = Array.from({ length: columnCount }, (_unused, columnIndex) => - '-'.repeat(columnWidths[columnIndex]) - ) - .map((dashes, columnIndex) => padCell(dashes, columnIndex)) - .join(' ') - const lines: string[] = [formatRow(headers), ruleRow] - for (const row of rows) { - lines.push(formatRow(row)) + const cell = row[columnIndex] || ""; + columnWidths[columnIndex] = Math.max( + columnWidths[columnIndex], + cell.length, + minColWidth, + ); } - return lines.join('\n') + } + const padCell = (cell: string, columnIndex: number) => + cell + " ".repeat(columnWidths[columnIndex] - cell.length); + const formatRow = (cells: string[]) => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + const ruleRow = Array.from({ length: columnCount }, (_unused, columnIndex) => + "-".repeat(columnWidths[columnIndex]), + ) + .map((dashes, columnIndex) => padCell(dashes, columnIndex)) + .join(" "); + const lines: string[] = [formatRow(headers), ruleRow]; + for (const row of rows) { + lines.push(formatRow(row)); + } + return lines.join("\n"); } export function renderRecordsListTable( - records: DRecord[], - options: { verbose?: boolean; columnWidth?: number } = {} + records: DRecord[], + options: { verbose?: boolean; columnWidth?: number } = {}, ): string { - return renderRecordsListAsciiTable(formatRecordsListTable(records, options)) + return renderRecordsListAsciiTable(formatRecordsListTable(records, options)); } diff --git a/KeeperSdk/src/roles/RoleManager.ts b/KeeperSdk/src/roles/RoleManager.ts index 673bb2c7..932f16e2 100644 --- a/KeeperSdk/src/roles/RoleManager.ts +++ b/KeeperSdk/src/roles/RoleManager.ts @@ -1,144 +1,163 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' -import { EnterpriseDataManager } from '../teams/enterpriseData' -import { formatRolesTable, listRoles, renderRolesAsciiTable } from './listRoles' +import type { Auth } from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; +import { EnterpriseDataManager } from "../teams/enterpriseData"; +import { + formatRolesTable, + listRoles, + renderRolesAsciiTable, +} from "./listRoles"; import type { - FormatRolesTableOptions, - FormattedRolesTable, - ListRoleRow, - ListRolesOptions, -} from './roleTypes' + FormatRolesTableOptions, + FormattedRolesTable, + ListRoleRow, + ListRolesOptions, +} from "./roleTypes"; import { - formatRoleView, - roleViewTable, - viewRole, - type FormatRoleViewOptions, - type FormattedRoleViewTable, - type RoleView, -} from './viewRole' + formatRoleView, + roleViewTable, + viewRole, + type FormatRoleViewOptions, + type FormattedRoleViewTable, + type RoleView, +} from "./viewRole"; import { - addRoles, - formatAddRoleResult, - renderAddRoleAsciiTable, - type AddRoleInput, - type AddRoleResult, - type FormattedAddRoleTable, -} from './addRole' + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + type AddRoleInput, + type AddRoleResult, + type FormattedAddRoleTable, +} from "./addRole"; import { - updateRoles, - formatUpdateRoleResult, - renderUpdateRoleAsciiTable, - type UpdateRoleInput, - type UpdateRoleResult, - type FormattedUpdateRoleTable, -} from './updateRole' + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + type UpdateRoleInput, + type UpdateRoleResult, + type FormattedUpdateRoleTable, +} from "./updateRole"; import { - deleteRoles, - formatDeleteRoleResult, - renderDeleteRoleAsciiTable, - type DeleteRoleInput, - type DeleteRoleResult, - type FormattedDeleteRoleTable, -} from './deleteRole' + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + type DeleteRoleInput, + type DeleteRoleResult, + type FormattedDeleteRoleTable, +} from "./deleteRole"; -export type AuthProvider = () => Auth +export type AuthProvider = () => Auth; export class RoleManager { - private readonly authProvider: AuthProvider - private enterpriseData: EnterpriseDataManager | null = null - - constructor(authProvider: AuthProvider) { - this.authProvider = authProvider - } - - public async listRoles(options: ListRolesOptions = {}): Promise { - return listRoles(this.requireAuth(), { - ...options, - enterpriseData: options.enterpriseData ?? this.getEnterpriseData(), - }) - } - - public formatRolesTable(rows: ListRoleRow[], options: FormatRolesTableOptions = {}): FormattedRolesTable { - return formatRolesTable(rows, options) - } - - public renderRolesAsciiTable( - table: FormattedRolesTable, - options: { minColWidth?: number } = {} - ): string { - return renderRolesAsciiTable(table, options) - } - - public async viewRole(identifier: string): Promise { - return viewRole(this.requireAuth(), identifier) - } - - public formatRoleView(view: RoleView, options: FormatRoleViewOptions = {}): FormattedRoleViewTable { - return formatRoleView(view, options) - } - - public roleViewTable(table: FormattedRoleViewTable): string { - return roleViewTable(table) - } - - public async addRoles(input: AddRoleInput): Promise { - const result = await addRoles(this.requireAuth(), input) - if (result.created > 0) this.invalidateEnterpriseData() - return result - } - - public formatAddRoleResult(result: AddRoleResult): FormattedAddRoleTable { - return formatAddRoleResult(result) - } - - public renderAddRoleAsciiTable(table: FormattedAddRoleTable): string { - return renderAddRoleAsciiTable(table) - } - - public async updateRoles(input: UpdateRoleInput): Promise { - const result = await updateRoles(this.requireAuth(), input) - if (result.updated > 0) this.invalidateEnterpriseData() - return result - } - - public formatUpdateRoleResult(result: UpdateRoleResult): FormattedUpdateRoleTable { - return formatUpdateRoleResult(result) - } - - public renderUpdateRoleAsciiTable(table: FormattedUpdateRoleTable): string { - return renderUpdateRoleAsciiTable(table) - } - - public async deleteRoles(input: DeleteRoleInput): Promise { - const result = await deleteRoles(this.requireAuth(), input) - if (result.deleted > 0) this.invalidateEnterpriseData() - return result - } - - public formatDeleteRoleResult(result: DeleteRoleResult): FormattedDeleteRoleTable { - return formatDeleteRoleResult(result) - } - - public renderDeleteRoleAsciiTable(table: FormattedDeleteRoleTable): string { - return renderDeleteRoleAsciiTable(table) - } - - private getEnterpriseData(): EnterpriseDataManager { - if (!this.enterpriseData) { - this.enterpriseData = new EnterpriseDataManager(this.requireAuth()) - } - return this.enterpriseData - } - - private invalidateEnterpriseData(): void { - this.enterpriseData?.clearCache() - } - - private requireAuth(): Auth { - const auth = this.authProvider() - if (!auth) { - throw new KeeperSdkError('You are not logged in. Please log in first.', ResultCodes.NOT_LOGGED_IN) - } - return auth - } + private readonly authProvider: AuthProvider; + private enterpriseData: EnterpriseDataManager | null = null; + + constructor(authProvider: AuthProvider) { + this.authProvider = authProvider; + } + + public async listRoles( + options: ListRolesOptions = {}, + ): Promise { + return listRoles(this.requireAuth(), { + ...options, + enterpriseData: options.enterpriseData ?? this.getEnterpriseData(), + }); + } + + public formatRolesTable( + rows: ListRoleRow[], + options: FormatRolesTableOptions = {}, + ): FormattedRolesTable { + return formatRolesTable(rows, options); + } + + public renderRolesAsciiTable( + table: FormattedRolesTable, + options: { minColWidth?: number } = {}, + ): string { + return renderRolesAsciiTable(table, options); + } + + public async viewRole(identifier: string): Promise { + return viewRole(this.requireAuth(), identifier); + } + + public formatRoleView( + view: RoleView, + options: FormatRoleViewOptions = {}, + ): FormattedRoleViewTable { + return formatRoleView(view, options); + } + + public roleViewTable(table: FormattedRoleViewTable): string { + return roleViewTable(table); + } + + public async addRoles(input: AddRoleInput): Promise { + const result = await addRoles(this.requireAuth(), input); + if (result.created > 0) this.invalidateEnterpriseData(); + return result; + } + + public formatAddRoleResult(result: AddRoleResult): FormattedAddRoleTable { + return formatAddRoleResult(result); + } + + public renderAddRoleAsciiTable(table: FormattedAddRoleTable): string { + return renderAddRoleAsciiTable(table); + } + + public async updateRoles(input: UpdateRoleInput): Promise { + const result = await updateRoles(this.requireAuth(), input); + if (result.updated > 0) this.invalidateEnterpriseData(); + return result; + } + + public formatUpdateRoleResult( + result: UpdateRoleResult, + ): FormattedUpdateRoleTable { + return formatUpdateRoleResult(result); + } + + public renderUpdateRoleAsciiTable(table: FormattedUpdateRoleTable): string { + return renderUpdateRoleAsciiTable(table); + } + + public async deleteRoles(input: DeleteRoleInput): Promise { + const result = await deleteRoles(this.requireAuth(), input); + if (result.deleted > 0) this.invalidateEnterpriseData(); + return result; + } + + public formatDeleteRoleResult( + result: DeleteRoleResult, + ): FormattedDeleteRoleTable { + return formatDeleteRoleResult(result); + } + + public renderDeleteRoleAsciiTable(table: FormattedDeleteRoleTable): string { + return renderDeleteRoleAsciiTable(table); + } + + private getEnterpriseData(): EnterpriseDataManager { + if (!this.enterpriseData) { + this.enterpriseData = new EnterpriseDataManager(this.requireAuth()); + } + return this.enterpriseData; + } + + private invalidateEnterpriseData(): void { + this.enterpriseData?.clearCache(); + } + + private requireAuth(): Auth { + const auth = this.authProvider(); + if (!auth) { + throw new KeeperSdkError( + "You are not logged in. Please log in first.", + ResultCodes.NOT_LOGGED_IN, + ); + } + return auth; + } } diff --git a/KeeperSdk/src/roles/addRole.ts b/KeeperSdk/src/roles/addRole.ts index 4804110c..6e915a74 100644 --- a/KeeperSdk/src/roles/addRole.ts +++ b/KeeperSdk/src/roles/addRole.ts @@ -1,230 +1,322 @@ import { - encryptObjectForStorage, - enterpriseAllocateIdsCommand, - roleAddCommand, - type Auth, - type RoleEditRequest, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' -import { EnterpriseDataInclude, EnterpriseDataManager, type EnterpriseRole } from '../teams/enterpriseData' + encryptObjectForStorage, + enterpriseAllocateIdsCommand, + roleAddCommand, + type Auth, + type RoleEditRequest, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; import { - applyDecryptedNodeNames, - applyEnterpriseNameToRoot, - parentNeedsNameLookup, - resolveParentNode, -} from '../teams/teamUtils' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseRole, +} from "../teams/enterpriseData"; import { - applyDecryptedRoleNames, - applyRoleEnforcements, - assertCommandSucceeded, - buildRoleResultRows, - buildRolesByLowerName, - nodePathOrFallback, - normalizeIdentifiers, - parseEnforcements, - renderRoleResultTable, - resolveToggle, - ROLE_TABLE_HEADERS, - validateRoleName, - type RoleToggleInput, -} from './roleUtils' - -const ADD_ROLE_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Nodes, EnterpriseDataInclude.Roles] + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveParentNode, +} from "../teams/teamUtils"; +import { + applyDecryptedRoleNames, + applyRoleEnforcements, + assertCommandSucceeded, + buildRoleResultRows, + buildRolesByLowerName, + nodePathOrFallback, + normalizeIdentifiers, + parseEnforcements, + renderRoleResultTable, + resolveToggle, + ROLE_TABLE_HEADERS, + validateRoleName, + type RoleToggleInput, +} from "./roleUtils"; + +const ADD_ROLE_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Roles, +]; export enum AddRoleStatus { - Created = 'created', - Skipped = 'skipped', - Failed = 'failed', + Created = "created", + Skipped = "skipped", + Failed = "failed", } export enum AddRoleSkipReason { - AlreadyExistsInParent = 'already_exists_in_parent', - ExistsElsewhereDeclined = 'exists_elsewhere_declined', + AlreadyExistsInParent = "already_exists_in_parent", + ExistsElsewhereDeclined = "exists_elsewhere_declined", } export type AddRoleConflictPrompt = { - roleName: string - parentNodeId: number - parentNodeName: string - existingRoleId: number - existingRoleName: string - existingNodeId: number -} + roleName: string; + parentNodeId: number; + parentNodeName: string; + existingRoleId: number; + existingRoleName: string; + existingNodeId: number; +}; -export type AddRoleConfirm = (prompt: AddRoleConflictPrompt) => boolean | Promise +export type AddRoleConfirm = ( + prompt: AddRoleConflictPrompt, +) => boolean | Promise; export type AddRoleInput = { - roles: string[] - parent?: string | number | null - newUser?: RoleToggleInput - visibleBelow?: RoleToggleInput - enforcements?: string[] - force?: boolean - confirm?: AddRoleConfirm -} + roles: string[]; + parent?: string | number | null; + newUser?: RoleToggleInput; + visibleBelow?: RoleToggleInput; + enforcements?: string[]; + force?: boolean; + confirm?: AddRoleConfirm; +}; export type AddRoleItemResult = { - roleId: number - roleName: string - nodeId: number - status: AddRoleStatus - skipReason?: AddRoleSkipReason - message?: string -} + roleId: number; + roleName: string; + nodeId: number; + status: AddRoleStatus; + skipReason?: AddRoleSkipReason; + message?: string; +}; export type AddRoleResult = { - success: boolean - parentNodeId: number - parentNodeName: string - items: AddRoleItemResult[] - created: number - skipped: number - failed: number -} + success: boolean; + parentNodeId: number; + parentNodeName: string; + items: AddRoleItemResult[]; + created: number; + skipped: number; + failed: number; +}; export type FormattedAddRoleTable = { - headers: string[] - rows: string[][] - parentNodeName: string - summary: string -} + headers: string[]; + rows: string[][]; + parentNodeName: string; + summary: string; +}; + +export async function addRoles( + auth: Auth, + input: AddRoleInput, +): Promise { + const requestedNames = normalizeIdentifiers(input.roles); + if (requestedNames.length === 0) { + throw new KeeperSdkError("No roles to add.", ResultCodes.NO_ROLES_TO_ADD); + } + requestedNames.forEach(validateRoleName); + + const force = input.force === true; + const newUserInherit = resolveToggle(input.newUser) ?? false; + const visibleBelow = resolveToggle(input.visibleBelow) ?? false; + const enforcements = parseEnforcements(input.enforcements ?? []); -export async function addRoles(auth: Auth, input: AddRoleInput): Promise { - const requestedNames = normalizeIdentifiers(input.roles) - if (requestedNames.length === 0) { - throw new KeeperSdkError('No roles to add.', ResultCodes.NO_ROLES_TO_ADD) + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(ADD_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]); + + const nodes = response.nodes || []; + applyDecryptedNodeNames(nodes, displayNames.nodes); + applyEnterpriseNameToRoot(nodes, response.enterprise_name); + if (parentNeedsNameLookup(input.parent ?? null)) + await enterpriseData.decryptNodeNames(nodes); + + const roles = response.roles || []; + applyDecryptedRoleNames(roles, displayNames.roles); + const rolesByLowerName = buildRolesByLowerName(roles); + + const parentNode = resolveParentNode(nodes, input.parent ?? null); + const parentNodeId = parentNode.node_id; + const parentNodeName = nodePathOrFallback(nodes, parentNode); + + const treeKey = await enterpriseData.getTreeKey(); + if (!treeKey) { + throw new KeeperSdkError( + "Enterprise tree key is unavailable. The current user may not have permission to administer roles.", + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE, + ); + } + + const items: AddRoleItemResult[] = []; + const seenLowerNames = new Set(); + for (const name of requestedNames) { + const lower = name.toLowerCase(); + if (seenLowerNames.has(lower)) continue; + seenLowerNames.add(lower); + + const conflicts = rolesByLowerName.get(lower) || []; + const sameParent = conflicts.find((role) => role.node_id === parentNodeId); + if (sameParent) { + items.push( + skippedItem( + sameParent.role_id, + name, + sameParent.node_id, + AddRoleSkipReason.AlreadyExistsInParent, + `Role "${name}" already exists in parent node ${parentNodeId}.`, + ), + ); + continue; } - requestedNames.forEach(validateRoleName) - - const force = input.force === true - const newUserInherit = resolveToggle(input.newUser) ?? false - const visibleBelow = resolveToggle(input.visibleBelow) ?? false - const enforcements = parseEnforcements(input.enforcements ?? []) - - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(ADD_ROLE_INCLUDES), - enterpriseData.getDisplayNames(), - ]) - - const nodes = response.nodes || [] - applyDecryptedNodeNames(nodes, displayNames.nodes) - applyEnterpriseNameToRoot(nodes, response.enterprise_name) - if (parentNeedsNameLookup(input.parent ?? null)) await enterpriseData.decryptNodeNames(nodes) - - const roles = response.roles || [] - applyDecryptedRoleNames(roles, displayNames.roles) - const rolesByLowerName = buildRolesByLowerName(roles) - - const parentNode = resolveParentNode(nodes, input.parent ?? null) - const parentNodeId = parentNode.node_id - const parentNodeName = nodePathOrFallback(nodes, parentNode) - - const treeKey = await enterpriseData.getTreeKey() - if (!treeKey) { - throw new KeeperSdkError( - 'Enterprise tree key is unavailable. The current user may not have permission to administer roles.', - ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE - ) + + if (conflicts.length > 0 && !force) { + const confirmed = await confirmCrossNode(input.confirm, conflicts[0], { + roleName: name, + parentNodeId, + parentNodeName, + }); + if (!confirmed) { + items.push( + skippedItem( + conflicts[0].role_id, + name, + conflicts[0].node_id, + AddRoleSkipReason.ExistsElsewhereDeclined, + `Role "${name}" exists in node ${conflicts[0].node_id}; creation declined.`, + ), + ); + continue; + } } - const items: AddRoleItemResult[] = [] - const seenLowerNames = new Set() - for (const name of requestedNames) { - const lower = name.toLowerCase() - if (seenLowerNames.has(lower)) continue - seenLowerNames.add(lower) - - const conflicts = rolesByLowerName.get(lower) || [] - const sameParent = conflicts.find((role) => role.node_id === parentNodeId) - if (sameParent) { - items.push(skippedItem(sameParent.role_id, name, sameParent.node_id, AddRoleSkipReason.AlreadyExistsInParent, - `Role "${name}" already exists in parent node ${parentNodeId}.`)) - continue - } - - if (conflicts.length > 0 && !force) { - const confirmed = await confirmCrossNode(input.confirm, conflicts[0], { roleName: name, parentNodeId, parentNodeName }) - if (!confirmed) { - items.push(skippedItem(conflicts[0].role_id, name, conflicts[0].node_id, AddRoleSkipReason.ExistsElsewhereDeclined, - `Role "${name}" exists in node ${conflicts[0].node_id}; creation declined.`)) - continue - } - } - - try { - const roleId = await allocateRoleId(auth) - const encryptedData = await encryptObjectForStorage({ displayname: name }, treeKey) - await sendRoleAdd(auth, { - role_id: roleId, - node_id: parentNodeId, - encrypted_data: encryptedData, - visible_below: visibleBelow, - new_user_inherit: newUserInherit, - }) - await applyRoleEnforcements(auth, roleId, enforcements) - items.push({ roleId, roleName: name, nodeId: parentNodeId, status: AddRoleStatus.Created }) - } catch (err) { - items.push({ roleId: 0, roleName: name, nodeId: parentNodeId, status: AddRoleStatus.Failed, message: extractErrorMessage(err) }) - } + try { + const roleId = await allocateRoleId(auth); + const encryptedData = await encryptObjectForStorage( + { displayname: name }, + treeKey, + ); + await sendRoleAdd(auth, { + role_id: roleId, + node_id: parentNodeId, + encrypted_data: encryptedData, + visible_below: visibleBelow, + new_user_inherit: newUserInherit, + }); + await applyRoleEnforcements(auth, roleId, enforcements); + items.push({ + roleId, + roleName: name, + nodeId: parentNodeId, + status: AddRoleStatus.Created, + }); + } catch (err) { + items.push({ + roleId: 0, + roleName: name, + nodeId: parentNodeId, + status: AddRoleStatus.Failed, + message: extractErrorMessage(err), + }); } + } - return finalizeResult(items, parentNodeId, parentNodeName) + return finalizeResult(items, parentNodeId, parentNodeName); } -export function formatAddRoleResult(result: AddRoleResult): FormattedAddRoleTable { - return { - headers: [...ROLE_TABLE_HEADERS], - rows: buildRoleResultRows(result.items, (item) => item.message || (item.skipReason ?? '')), - parentNodeName: result.parentNodeName, - summary: `Created: ${result.created} Skipped: ${result.skipped} Failed: ${result.failed}`, - } +export function formatAddRoleResult( + result: AddRoleResult, +): FormattedAddRoleTable { + return { + headers: [...ROLE_TABLE_HEADERS], + rows: buildRoleResultRows( + result.items, + (item) => item.message || (item.skipReason ?? ""), + ), + parentNodeName: result.parentNodeName, + summary: `Created: ${result.created} Skipped: ${result.skipped} Failed: ${result.failed}`, + }; } export function renderAddRoleAsciiTable(table: FormattedAddRoleTable): string { - return renderRoleResultTable(table.headers, table.rows, table.summary, [`Parent: ${table.parentNodeName}`]) + return renderRoleResultTable(table.headers, table.rows, table.summary, [ + `Parent: ${table.parentNodeName}`, + ]); } async function allocateRoleId(auth: Auth): Promise { - const response = await auth.executeRestCommand(enterpriseAllocateIdsCommand({ number_requested: 1 })) - if (!response.base_id) { - throw new KeeperSdkError('Failed to allocate enterprise ID for new role.', ResultCodes.ROLE_ID_ALLOCATION_FAILED) - } - return response.base_id + const response = await auth.executeRestCommand( + enterpriseAllocateIdsCommand({ number_requested: 1 }), + ); + if (!response.base_id) { + throw new KeeperSdkError( + "Failed to allocate enterprise ID for new role.", + ResultCodes.ROLE_ID_ALLOCATION_FAILED, + ); + } + return response.base_id; } -async function sendRoleAdd(auth: Auth, payload: RoleEditRequest): Promise { - const response = await auth.executeRestCommand(roleAddCommand(payload)) - assertCommandSucceeded(response, `role_add failed for role_id=${payload.role_id}`, ResultCodes.ROLE_ADD_FAILED) +async function sendRoleAdd( + auth: Auth, + payload: RoleEditRequest, +): Promise { + const response = await auth.executeRestCommand(roleAddCommand(payload)); + assertCommandSucceeded( + response, + `role_add failed for role_id=${payload.role_id}`, + ResultCodes.ROLE_ADD_FAILED, + ); } async function confirmCrossNode( - confirm: AddRoleConfirm | undefined, - existing: EnterpriseRole, - context: { roleName: string; parentNodeId: number; parentNodeName: string } + confirm: AddRoleConfirm | undefined, + existing: EnterpriseRole, + context: { roleName: string; parentNodeId: number; parentNodeName: string }, ): Promise { - if (!confirm) return false - return (await confirm({ - ...context, - existingRoleId: existing.role_id, - existingRoleName: (existing.displayName || '').trim() || String(existing.role_id), - existingNodeId: existing.node_id ?? 0, + if (!confirm) return false; + return ( + (await confirm({ + ...context, + existingRoleId: existing.role_id, + existingRoleName: + (existing.displayName || "").trim() || String(existing.role_id), + existingNodeId: existing.node_id ?? 0, })) === true + ); } function skippedItem( - roleId: number, - roleName: string, - nodeId: number, - skipReason: AddRoleSkipReason, - message: string + roleId: number, + roleName: string, + nodeId: number, + skipReason: AddRoleSkipReason, + message: string, ): AddRoleItemResult { - return { roleId, roleName, nodeId, status: AddRoleStatus.Skipped, skipReason, message } + return { + roleId, + roleName, + nodeId, + status: AddRoleStatus.Skipped, + skipReason, + message, + }; } -function finalizeResult(items: AddRoleItemResult[], parentNodeId: number, parentNodeName: string): AddRoleResult { - const created = items.filter((item) => item.status === AddRoleStatus.Created).length - const skipped = items.filter((item) => item.status === AddRoleStatus.Skipped).length - const failed = items.filter((item) => item.status === AddRoleStatus.Failed).length - return { success: failed === 0 && created > 0, parentNodeId, parentNodeName, items, created, skipped, failed } +function finalizeResult( + items: AddRoleItemResult[], + parentNodeId: number, + parentNodeName: string, +): AddRoleResult { + const created = items.filter( + (item) => item.status === AddRoleStatus.Created, + ).length; + const skipped = items.filter( + (item) => item.status === AddRoleStatus.Skipped, + ).length; + const failed = items.filter( + (item) => item.status === AddRoleStatus.Failed, + ).length; + return { + success: failed === 0 && created > 0, + parentNodeId, + parentNodeName, + items, + created, + skipped, + failed, + }; } diff --git a/KeeperSdk/src/roles/deleteRole.ts b/KeeperSdk/src/roles/deleteRole.ts index 9a893a73..b0b83f4c 100644 --- a/KeeperSdk/src/roles/deleteRole.ts +++ b/KeeperSdk/src/roles/deleteRole.ts @@ -1,98 +1,134 @@ -import { roleDeleteCommand, type Auth } from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' -import { EnterpriseDataInclude, EnterpriseDataManager } from '../teams/enterpriseData' +import { roleDeleteCommand, type Auth } from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; import { - applyDecryptedRoleNames, - assertCommandSucceeded, - buildRoleResultRows, - normalizeIdentifiers, - renderRoleResultTable, - resolveExistingRoles, - ROLE_TABLE_HEADERS, -} from './roleUtils' + EnterpriseDataInclude, + EnterpriseDataManager, +} from "../teams/enterpriseData"; +import { + applyDecryptedRoleNames, + assertCommandSucceeded, + buildRoleResultRows, + normalizeIdentifiers, + renderRoleResultTable, + resolveExistingRoles, + ROLE_TABLE_HEADERS, +} from "./roleUtils"; -const DELETE_ROLE_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Roles] +const DELETE_ROLE_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Roles, +]; export enum DeleteRoleStatus { - Deleted = 'deleted', - Failed = 'failed', + Deleted = "deleted", + Failed = "failed", } export type DeleteRoleInput = { - roles: string[] -} + roles: string[]; +}; export type DeleteRoleItemResult = { - roleId: number - roleName: string - nodeId: number - status: DeleteRoleStatus - message?: string -} + roleId: number; + roleName: string; + nodeId: number; + status: DeleteRoleStatus; + message?: string; +}; export type DeleteRoleResult = { - success: boolean - items: DeleteRoleItemResult[] - deleted: number - failed: number -} + success: boolean; + items: DeleteRoleItemResult[]; + deleted: number; + failed: number; +}; export type FormattedDeleteRoleTable = { - headers: string[] - rows: string[][] - summary: string -} + headers: string[]; + rows: string[][]; + summary: string; +}; -export async function deleteRoles(auth: Auth, input: DeleteRoleInput): Promise { - const identifiers = normalizeIdentifiers(input.roles) - if (identifiers.length === 0) { - throw new KeeperSdkError('No roles to delete.', ResultCodes.NO_ROLES_TO_DELETE) - } +export async function deleteRoles( + auth: Auth, + input: DeleteRoleInput, +): Promise { + const identifiers = normalizeIdentifiers(input.roles); + if (identifiers.length === 0) { + throw new KeeperSdkError( + "No roles to delete.", + ResultCodes.NO_ROLES_TO_DELETE, + ); + } - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(DELETE_ROLE_INCLUDES), - enterpriseData.getDisplayNames(), - ]) + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(DELETE_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]); - const roles = response.roles || [] - applyDecryptedRoleNames(roles, displayNames.roles) - const resolvedRoles = resolveExistingRoles(roles, identifiers) + const roles = response.roles || []; + applyDecryptedRoleNames(roles, displayNames.roles); + const resolvedRoles = resolveExistingRoles(roles, identifiers); - const items: DeleteRoleItemResult[] = [] - for (const role of resolvedRoles) { - const roleName = (role.displayName || '').trim() || String(role.role_id) - const nodeId = role.node_id ?? 0 - try { - await sendRoleDelete(auth, role.role_id) - items.push({ roleId: role.role_id, roleName, nodeId, status: DeleteRoleStatus.Deleted }) - } catch (err) { - items.push({ roleId: role.role_id, roleName, nodeId, status: DeleteRoleStatus.Failed, message: extractErrorMessage(err) }) - } + const items: DeleteRoleItemResult[] = []; + for (const role of resolvedRoles) { + const roleName = (role.displayName || "").trim() || String(role.role_id); + const nodeId = role.node_id ?? 0; + try { + await sendRoleDelete(auth, role.role_id); + items.push({ + roleId: role.role_id, + roleName, + nodeId, + status: DeleteRoleStatus.Deleted, + }); + } catch (err) { + items.push({ + roleId: role.role_id, + roleName, + nodeId, + status: DeleteRoleStatus.Failed, + message: extractErrorMessage(err), + }); } + } - return finalizeResult(items) + return finalizeResult(items); } -export function formatDeleteRoleResult(result: DeleteRoleResult): FormattedDeleteRoleTable { - return { - headers: [...ROLE_TABLE_HEADERS], - rows: buildRoleResultRows(result.items, (item) => item.message || ''), - summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, - } +export function formatDeleteRoleResult( + result: DeleteRoleResult, +): FormattedDeleteRoleTable { + return { + headers: [...ROLE_TABLE_HEADERS], + rows: buildRoleResultRows(result.items, (item) => item.message || ""), + summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, + }; } -export function renderDeleteRoleAsciiTable(table: FormattedDeleteRoleTable): string { - return renderRoleResultTable(table.headers, table.rows, table.summary) +export function renderDeleteRoleAsciiTable( + table: FormattedDeleteRoleTable, +): string { + return renderRoleResultTable(table.headers, table.rows, table.summary); } async function sendRoleDelete(auth: Auth, roleId: number): Promise { - const response = await auth.executeRestCommand(roleDeleteCommand({ role_id: roleId })) - assertCommandSucceeded(response, `role_delete failed for role_id=${roleId}`, ResultCodes.ROLE_DELETE_FAILED) + const response = await auth.executeRestCommand( + roleDeleteCommand({ role_id: roleId }), + ); + assertCommandSucceeded( + response, + `role_delete failed for role_id=${roleId}`, + ResultCodes.ROLE_DELETE_FAILED, + ); } function finalizeResult(items: DeleteRoleItemResult[]): DeleteRoleResult { - const deleted = items.filter((item) => item.status === DeleteRoleStatus.Deleted).length - const failed = items.filter((item) => item.status === DeleteRoleStatus.Failed).length - return { success: failed === 0 && deleted > 0, items, deleted, failed } + const deleted = items.filter( + (item) => item.status === DeleteRoleStatus.Deleted, + ).length; + const failed = items.filter( + (item) => item.status === DeleteRoleStatus.Failed, + ).length; + return { success: failed === 0 && deleted > 0, items, deleted, failed }; } diff --git a/KeeperSdk/src/roles/index.ts b/KeeperSdk/src/roles/index.ts index b0e3b993..9d951a94 100644 --- a/KeeperSdk/src/roles/index.ts +++ b/KeeperSdk/src/roles/index.ts @@ -1,74 +1,78 @@ -export { listRoles, formatRolesTable, renderRolesAsciiTable } from './listRoles' +export { + listRoles, + formatRolesTable, + renderRolesAsciiTable, +} from "./listRoles"; export { - RoleColumn, - SUPPORTED_ROLE_COLUMNS, - DEFAULT_ROLE_COLUMNS, - ALL_COLUMNS_WILDCARD, -} from './roleTypes' + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + DEFAULT_ROLE_COLUMNS, + ALL_COLUMNS_WILDCARD, +} from "./roleTypes"; export type { - ListRolesOptions, - ListRoleRow, - RoleColumnInput, - FormattedRolesTable, - FormatRolesTableOptions, -} from './roleTypes' + ListRolesOptions, + ListRoleRow, + RoleColumnInput, + FormattedRolesTable, + FormatRolesTableOptions, +} from "./roleTypes"; -export { viewRole, formatRoleView, roleViewTable } from './viewRole' +export { viewRole, formatRoleView, roleViewTable } from "./viewRole"; export type { - RoleView, - RoleTeamInfo, - RoleUserInfo, - RoleManagedNodeInfo, - RoleEnforcementInfo, - FormatRoleViewOptions, - FormattedRoleViewTable, - FormattedManagedNodePrivilegeTable, - RoleViewTableRow, -} from './viewRole' + RoleView, + RoleTeamInfo, + RoleUserInfo, + RoleManagedNodeInfo, + RoleEnforcementInfo, + FormatRoleViewOptions, + FormattedRoleViewTable, + FormattedManagedNodePrivilegeTable, + RoleViewTableRow, +} from "./viewRole"; -export { RoleManager } from './RoleManager' +export { RoleManager } from "./RoleManager"; export { - addRoles, - formatAddRoleResult, - renderAddRoleAsciiTable, - AddRoleStatus, - AddRoleSkipReason, -} from './addRole' + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + AddRoleStatus, + AddRoleSkipReason, +} from "./addRole"; export type { - AddRoleInput, - AddRoleResult, - AddRoleItemResult, - AddRoleConfirm, - AddRoleConflictPrompt, - FormattedAddRoleTable, -} from './addRole' + AddRoleInput, + AddRoleResult, + AddRoleItemResult, + AddRoleConfirm, + AddRoleConflictPrompt, + FormattedAddRoleTable, +} from "./addRole"; export { - updateRoles, - formatUpdateRoleResult, - renderUpdateRoleAsciiTable, - UpdateRoleStatus, -} from './updateRole' + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + UpdateRoleStatus, +} from "./updateRole"; export type { - UpdateRoleInput, - UpdateRoleResult, - UpdateRoleItemResult, - FormattedUpdateRoleTable, -} from './updateRole' + UpdateRoleInput, + UpdateRoleResult, + UpdateRoleItemResult, + FormattedUpdateRoleTable, +} from "./updateRole"; export { - deleteRoles, - formatDeleteRoleResult, - renderDeleteRoleAsciiTable, - DeleteRoleStatus, -} from './deleteRole' + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + DeleteRoleStatus, +} from "./deleteRole"; export type { - DeleteRoleInput, - DeleteRoleResult, - DeleteRoleItemResult, - FormattedDeleteRoleTable, -} from './deleteRole' + DeleteRoleInput, + DeleteRoleResult, + DeleteRoleItemResult, + FormattedDeleteRoleTable, +} from "./deleteRole"; -export type { RoleToggleInput, RoleToggle, EnforcementPair } from './roleUtils' +export type { RoleToggleInput, RoleToggle, EnforcementPair } from "./roleUtils"; diff --git a/KeeperSdk/src/roles/listRoles.ts b/KeeperSdk/src/roles/listRoles.ts index efa4c1d9..6a2a2cde 100644 --- a/KeeperSdk/src/roles/listRoles.ts +++ b/KeeperSdk/src/roles/listRoles.ts @@ -1,341 +1,388 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { isNumber, resolveSearchPattern, TOKEN_SEPARATOR_PATTERN } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseDataManagerApi, - type EnterpriseDisplayNames, - type EnterpriseNode, - type EnterpriseRole, - type EnterpriseRolePrivilegeLink, - type EnterpriseRoleTeamLink, - type EnterpriseRoleUserLink, - type EnterpriseTeamRecord, - type EnterpriseUser, -} from '../teams/enterpriseData' -import { applyDecryptedNodeNames } from '../teams/teamUtils' + isNumber, + resolveSearchPattern, + TOKEN_SEPARATOR_PATTERN, +} from "../utils"; import { - ALL_COLUMNS_WILDCARD, - DEFAULT_ROLE_COLUMNS, - RoleColumn, - SUPPORTED_ROLE_COLUMNS, - type FormatRolesTableOptions, - type FormattedRolesTable, - type ListRoleRow, - type ListRolesOptions, -} from './roleTypes' - -const NODE_PATH_SEPARATOR = '\\' -const MIN_ASCII_COL_WIDTH = 2 + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseDataManagerApi, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseRolePrivilegeLink, + type EnterpriseRoleTeamLink, + type EnterpriseRoleUserLink, + type EnterpriseTeamRecord, + type EnterpriseUser, +} from "../teams/enterpriseData"; +import { applyDecryptedNodeNames } from "../teams/teamUtils"; +import { + ALL_COLUMNS_WILDCARD, + DEFAULT_ROLE_COLUMNS, + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + type FormatRolesTableOptions, + type FormattedRolesTable, + type ListRoleRow, + type ListRolesOptions, +} from "./roleTypes"; + +const NODE_PATH_SEPARATOR = "\\"; +const MIN_ASCII_COL_WIDTH = 2; const HEADER_BY_COLUMN: Record = { - [RoleColumn.VisibleBelow]: 'Visible Below', - [RoleColumn.DefaultRole]: 'Default Role', - [RoleColumn.Admin]: 'Admin', - [RoleColumn.Node]: 'Node', - [RoleColumn.UserCount]: 'User Count', - [RoleColumn.Users]: 'Users', - [RoleColumn.TeamCount]: 'Team Count', - [RoleColumn.Teams]: 'Teams', -} + [RoleColumn.VisibleBelow]: "Visible Below", + [RoleColumn.DefaultRole]: "Default Role", + [RoleColumn.Admin]: "Admin", + [RoleColumn.Node]: "Node", + [RoleColumn.UserCount]: "User Count", + [RoleColumn.Users]: "Users", + [RoleColumn.TeamCount]: "Team Count", + [RoleColumn.Teams]: "Teams", +}; type DecorateContext = { - nodePaths: Map - adminRoleIds: Set - roleUsers: Map> - roleTeams: Map> - usernameById: Map - teamNameById: Map -} - -export async function listRoles(auth: Auth, options: ListRolesOptions = {}): Promise { - const columns = resolveColumns(options.columns) - const includes = includesForColumns(columns) - const wantsDisplayNames = - columns.includes(RoleColumn.Node) || - columns.includes(RoleColumn.Teams) || - columns.includes(RoleColumn.TeamCount) - - const enterpriseData: EnterpriseDataManagerApi = - options.enterpriseData ?? new EnterpriseDataManager(auth) - const emptyDisplayNames: EnterpriseDisplayNames = { nodes: new Map(), roles: new Map() } - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(includes), - wantsDisplayNames ? enterpriseData.getDisplayNames() : Promise.resolve(emptyDisplayNames), - ]) - - const roles = response.roles || [] - const nodes = response.nodes || [] - applyDecryptedNodeNames(nodes, displayNames.nodes) - - for (const role of roles) { - const display = displayNames.roles.get(role.role_id) - if (display) role.displayName = display - } - - const context: DecorateContext = { - nodePaths: buildNodePathLookup(nodes), - adminRoleIds: buildAdminRoleIds(response.role_privileges || []), - roleUsers: buildRoleUserMap(response.role_users || []), - roleTeams: buildRoleTeamMap(response.role_teams || []), - usernameById: buildUsernameMap(response.users || []), - teamNameById: buildTeamNameMap(response.teams || []), - } - - const pattern = resolveSearchPattern(options.pattern) - const rows: ListRoleRow[] = [] - for (const role of roles) { - const row = decorateRow(role, columns, context) - if (pattern && !rowMatchesPattern(row, pattern)) continue - rows.push(row) - } - - rows.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) - return rows + nodePaths: Map; + adminRoleIds: Set; + roleUsers: Map>; + roleTeams: Map>; + usernameById: Map; + teamNameById: Map; +}; + +export async function listRoles( + auth: Auth, + options: ListRolesOptions = {}, +): Promise { + const columns = resolveColumns(options.columns); + const includes = includesForColumns(columns); + const wantsDisplayNames = + columns.includes(RoleColumn.Node) || + columns.includes(RoleColumn.Teams) || + columns.includes(RoleColumn.TeamCount); + + const enterpriseData: EnterpriseDataManagerApi = + options.enterpriseData ?? new EnterpriseDataManager(auth); + const emptyDisplayNames: EnterpriseDisplayNames = { + nodes: new Map(), + roles: new Map(), + }; + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(includes), + wantsDisplayNames + ? enterpriseData.getDisplayNames() + : Promise.resolve(emptyDisplayNames), + ]); + + const roles = response.roles || []; + const nodes = response.nodes || []; + applyDecryptedNodeNames(nodes, displayNames.nodes); + + for (const role of roles) { + const display = displayNames.roles.get(role.role_id); + if (display) role.displayName = display; + } + + const context: DecorateContext = { + nodePaths: buildNodePathLookup(nodes), + adminRoleIds: buildAdminRoleIds(response.role_privileges || []), + roleUsers: buildRoleUserMap(response.role_users || []), + roleTeams: buildRoleTeamMap(response.role_teams || []), + usernameById: buildUsernameMap(response.users || []), + teamNameById: buildTeamNameMap(response.teams || []), + }; + + const pattern = resolveSearchPattern(options.pattern); + const rows: ListRoleRow[] = []; + for (const role of roles) { + const row = decorateRow(role, columns, context); + if (pattern && !rowMatchesPattern(row, pattern)) continue; + rows.push(row); + } + + rows.sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); + return rows; } export function formatRolesTable( - rows: ListRoleRow[], - options: FormatRolesTableOptions = {} + rows: ListRoleRow[], + options: FormatRolesTableOptions = {}, ): FormattedRolesTable { - const columns = resolveColumns(options.columns) - const headers: string[] = ['#', 'Role ID', 'Name', ...columns.map((col) => HEADER_BY_COLUMN[col])] - - const outRows: string[][] = rows.map((row, rowIndex) => { - const cells: string[] = [String(rowIndex + 1), String(row.role_id), row.name] - for (const col of columns) cells.push(formatCell(row, col)) - return cells - }) - - return { headers, rows: outRows } + const columns = resolveColumns(options.columns); + const headers: string[] = [ + "#", + "Role ID", + "Name", + ...columns.map((col) => HEADER_BY_COLUMN[col]), + ]; + + const outRows: string[][] = rows.map((row, rowIndex) => { + const cells: string[] = [ + String(rowIndex + 1), + String(row.role_id), + row.name, + ]; + for (const col of columns) cells.push(formatCell(row, col)); + return cells; + }); + + return { headers, rows: outRows }; } export function renderRolesAsciiTable( - table: FormattedRolesTable, - options: { minColWidth?: number } = {} + table: FormattedRolesTable, + options: { minColWidth?: number } = {}, ): string { - const { minColWidth = MIN_ASCII_COL_WIDTH } = options - const { headers, rows } = table - const columnCount = headers.length - - const expandedRows: string[][][] = rows.map((row) => - row.map((cell) => (cell.includes('\n') ? cell.split('\n') : [cell])) - ) - - const columnWidths = new Array(columnCount).fill(0) + const { minColWidth = MIN_ASCII_COL_WIDTH } = options; + const { headers, rows } = table; + const columnCount = headers.length; + + const expandedRows: string[][][] = rows.map((row) => + row.map((cell) => (cell.includes("\n") ? cell.split("\n") : [cell])), + ); + + const columnWidths = new Array(columnCount).fill(0); + for (let ci = 0; ci < columnCount; ci += 1) { + columnWidths[ci] = Math.max(headers[ci].length, minColWidth); + } + for (const row of expandedRows) { for (let ci = 0; ci < columnCount; ci += 1) { - columnWidths[ci] = Math.max(headers[ci].length, minColWidth) - } - for (const row of expandedRows) { - for (let ci = 0; ci < columnCount; ci += 1) { - for (const line of row[ci]) { - columnWidths[ci] = Math.max(columnWidths[ci], line.length, minColWidth) - } - } + for (const line of row[ci]) { + columnWidths[ci] = Math.max(columnWidths[ci], line.length, minColWidth); + } } + } - const padCell = (cell: string, ci: number): string => - cell + ' '.repeat(columnWidths[ci] - cell.length) - const formatPhysicalRow = (cells: string[]): string => - cells.map((cell, ci) => padCell(cell, ci)).join(' ') + const padCell = (cell: string, ci: number): string => + cell + " ".repeat(columnWidths[ci] - cell.length); + const formatPhysicalRow = (cells: string[]): string => + cells.map((cell, ci) => padCell(cell, ci)).join(" "); - const ruleRow = formatPhysicalRow(columnWidths.map((w) => '-'.repeat(w))) - const lines: string[] = [formatPhysicalRow(headers), ruleRow] + const ruleRow = formatPhysicalRow(columnWidths.map((w) => "-".repeat(w))); + const lines: string[] = [formatPhysicalRow(headers), ruleRow]; - for (const row of expandedRows) { - const physicalLineCount = Math.max(...row.map((cell) => cell.length)) - for (let li = 0; li < physicalLineCount; li += 1) { - lines.push(formatPhysicalRow(row.map((cell) => cell[li] ?? ''))) - } + for (const row of expandedRows) { + const physicalLineCount = Math.max(...row.map((cell) => cell.length)); + for (let li = 0; li < physicalLineCount; li += 1) { + lines.push(formatPhysicalRow(row.map((cell) => cell[li] ?? ""))); } - return lines.join('\n') + } + return lines.join("\n"); } -function includesForColumns(columns: readonly RoleColumn[]): EnterpriseDataInclude[] { - const set = new Set([EnterpriseDataInclude.Roles]) - for (const col of columns) { - switch (col) { - case RoleColumn.Node: - set.add(EnterpriseDataInclude.Nodes) - break - case RoleColumn.Admin: - set.add(EnterpriseDataInclude.RolePrivileges) - break - case RoleColumn.UserCount: - case RoleColumn.Users: - set.add(EnterpriseDataInclude.RoleUsers) - if (col === RoleColumn.Users) set.add(EnterpriseDataInclude.Users) - break - case RoleColumn.TeamCount: - case RoleColumn.Teams: - set.add(EnterpriseDataInclude.RoleTeams) - if (col === RoleColumn.Teams) set.add(EnterpriseDataInclude.Teams) - break - } +function includesForColumns( + columns: readonly RoleColumn[], +): EnterpriseDataInclude[] { + const set = new Set([EnterpriseDataInclude.Roles]); + for (const col of columns) { + switch (col) { + case RoleColumn.Node: + set.add(EnterpriseDataInclude.Nodes); + break; + case RoleColumn.Admin: + set.add(EnterpriseDataInclude.RolePrivileges); + break; + case RoleColumn.UserCount: + case RoleColumn.Users: + set.add(EnterpriseDataInclude.RoleUsers); + if (col === RoleColumn.Users) set.add(EnterpriseDataInclude.Users); + break; + case RoleColumn.TeamCount: + case RoleColumn.Teams: + set.add(EnterpriseDataInclude.RoleTeams); + if (col === RoleColumn.Teams) set.add(EnterpriseDataInclude.Teams); + break; } - return Array.from(set) + } + return Array.from(set); } -function resolveColumns(input: ListRolesOptions['columns']): RoleColumn[] { - if (input == null) return [...DEFAULT_ROLE_COLUMNS] - if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_ROLE_COLUMNS] - - const requested = Array.isArray(input) ? input : input.split(',').map((p) => p.trim()) - const allowed = new Set(SUPPORTED_ROLE_COLUMNS) - const seen = new Set() - for (const col of requested) { - if (col && allowed.has(col)) seen.add(col as RoleColumn) - } - if (seen.size === 0) return [...DEFAULT_ROLE_COLUMNS] - return SUPPORTED_ROLE_COLUMNS.filter((col) => seen.has(col)) +function resolveColumns(input: ListRolesOptions["columns"]): RoleColumn[] { + if (input == null) return [...DEFAULT_ROLE_COLUMNS]; + if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_ROLE_COLUMNS]; + + const requested = Array.isArray(input) + ? input + : input.split(",").map((p) => p.trim()); + const allowed = new Set(SUPPORTED_ROLE_COLUMNS); + const seen = new Set(); + for (const col of requested) { + if (col && allowed.has(col)) seen.add(col as RoleColumn); + } + if (seen.size === 0) return [...DEFAULT_ROLE_COLUMNS]; + return SUPPORTED_ROLE_COLUMNS.filter((col) => seen.has(col)); } function roleDisplayName(role: EnterpriseRole): string { - return (role.displayName || '').trim() || String(role.role_id) + return (role.displayName || "").trim() || String(role.role_id); } function tokenize(text: string): string[] { - return text.split(TOKEN_SEPARATOR_PATTERN).filter((t) => t.length > 0) + return text.split(TOKEN_SEPARATOR_PATTERN).filter((t) => t.length > 0); } function rowMatchesPattern(row: ListRoleRow, pattern: string): boolean { - const lower = pattern.toLowerCase() - const tokens: string[] = [String(row.role_id), ...tokenize(row.name.toLowerCase())] - if (row.node) tokens.push(...tokenize(row.node.toLowerCase())) - if (row.user_count != null) tokens.push(String(row.user_count)) - if (row.team_count != null) tokens.push(String(row.team_count)) - for (const list of [row.users, row.teams]) { - if (!list) continue - for (const v of list) tokens.push(...tokenize(v.toLowerCase())) - } - return tokens.some((t) => t.includes(lower)) + const lower = pattern.toLowerCase(); + const tokens: string[] = [ + String(row.role_id), + ...tokenize(row.name.toLowerCase()), + ]; + if (row.node) tokens.push(...tokenize(row.node.toLowerCase())); + if (row.user_count != null) tokens.push(String(row.user_count)); + if (row.team_count != null) tokens.push(String(row.team_count)); + for (const list of [row.users, row.teams]) { + if (!list) continue; + for (const v of list) tokens.push(...tokenize(v.toLowerCase())); + } + return tokens.some((t) => t.includes(lower)); } function buildNodePathLookup(nodes: EnterpriseNode[]): Map { - return new Map( - nodes.map((node) => [ - node.node_id, - EnterpriseDataManager.getNodePath(nodes, node.node_id, { separator: NODE_PATH_SEPARATOR }), - ]) - ) + return new Map( + nodes.map((node) => [ + node.node_id, + EnterpriseDataManager.getNodePath(nodes, node.node_id, { + separator: NODE_PATH_SEPARATOR, + }), + ]), + ); } -function buildAdminRoleIds(privileges: EnterpriseRolePrivilegeLink[]): Set { - const set = new Set() - for (const p of privileges) set.add(p.role_id) - return set +function buildAdminRoleIds( + privileges: EnterpriseRolePrivilegeLink[], +): Set { + const set = new Set(); + for (const p of privileges) set.add(p.role_id); + return set; } -function buildRoleUserMap(links: EnterpriseRoleUserLink[]): Map> { - const map = new Map>() - for (const link of links) { - const set = map.get(link.role_id) - if (set) set.add(link.enterprise_user_id) - else map.set(link.role_id, new Set([link.enterprise_user_id])) - } - return map +function buildRoleUserMap( + links: EnterpriseRoleUserLink[], +): Map> { + const map = new Map>(); + for (const link of links) { + const set = map.get(link.role_id); + if (set) set.add(link.enterprise_user_id); + else map.set(link.role_id, new Set([link.enterprise_user_id])); + } + return map; } -function buildRoleTeamMap(links: EnterpriseRoleTeamLink[]): Map> { - const map = new Map>() - for (const link of links) { - const set = map.get(link.role_id) - if (set) set.add(link.team_uid) - else map.set(link.role_id, new Set([link.team_uid])) - } - return map +function buildRoleTeamMap( + links: EnterpriseRoleTeamLink[], +): Map> { + const map = new Map>(); + for (const link of links) { + const set = map.get(link.role_id); + if (set) set.add(link.team_uid); + else map.set(link.role_id, new Set([link.team_uid])); + } + return map; } function buildUsernameMap(users: EnterpriseUser[]): Map { - const map = new Map() - for (const user of users) { - if (isNumber(user.enterprise_user_id) && user.username) { - map.set(user.enterprise_user_id, user.username) - } + const map = new Map(); + for (const user of users) { + if (isNumber(user.enterprise_user_id) && user.username) { + map.set(user.enterprise_user_id, user.username); } - return map + } + return map; } function buildTeamNameMap(teams: EnterpriseTeamRecord[]): Map { - const map = new Map() - for (const team of teams) { - if (team.team_uid) map.set(team.team_uid, (team.name || team.team_uid).trim()) - } - return map + const map = new Map(); + for (const team of teams) { + if (team.team_uid) + map.set(team.team_uid, (team.name || team.team_uid).trim()); + } + return map; } -function resolveSortedNames(ids: Set, nameById: Map): string[] { - const result: string[] = [] - for (const id of ids) { - const name = nameById.get(id) - if (name) result.push(name) - } - return result.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) +function resolveSortedNames( + ids: Set, + nameById: Map, +): string[] { + const result: string[] = []; + for (const id of ids) { + const name = nameById.get(id); + if (name) result.push(name); + } + return result.sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ); } function decorateRow( - role: EnterpriseRole, - columns: readonly RoleColumn[], - context: DecorateContext + role: EnterpriseRole, + columns: readonly RoleColumn[], + context: DecorateContext, ): ListRoleRow { - const row: ListRoleRow = { role_id: role.role_id, name: roleDisplayName(role) } - - for (const col of columns) { - switch (col) { - case RoleColumn.VisibleBelow: - row.visible_below = role.visible_below ?? false - break - case RoleColumn.DefaultRole: - row.default_role = role.new_user_inherit ?? false - break - case RoleColumn.Admin: - row.admin = context.adminRoleIds.has(role.role_id) - break - case RoleColumn.Node: - row.node = context.nodePaths.get(role.node_id ?? 0) ?? '' - break - case RoleColumn.UserCount: - row.user_count = context.roleUsers.get(role.role_id)?.size ?? 0 - break - case RoleColumn.Users: { - const ids = context.roleUsers.get(role.role_id) - row.users = ids ? resolveSortedNames(ids, context.usernameById) : [] - break - } - case RoleColumn.TeamCount: - row.team_count = context.roleTeams.get(role.role_id)?.size ?? 0 - break - case RoleColumn.Teams: { - const uids = context.roleTeams.get(role.role_id) - row.teams = uids ? resolveSortedNames(uids, context.teamNameById) : [] - break - } - } + const row: ListRoleRow = { + role_id: role.role_id, + name: roleDisplayName(role), + }; + + for (const col of columns) { + switch (col) { + case RoleColumn.VisibleBelow: + row.visible_below = role.visible_below ?? false; + break; + case RoleColumn.DefaultRole: + row.default_role = role.new_user_inherit ?? false; + break; + case RoleColumn.Admin: + row.admin = context.adminRoleIds.has(role.role_id); + break; + case RoleColumn.Node: + row.node = context.nodePaths.get(role.node_id ?? 0) ?? ""; + break; + case RoleColumn.UserCount: + row.user_count = context.roleUsers.get(role.role_id)?.size ?? 0; + break; + case RoleColumn.Users: { + const ids = context.roleUsers.get(role.role_id); + row.users = ids ? resolveSortedNames(ids, context.usernameById) : []; + break; + } + case RoleColumn.TeamCount: + row.team_count = context.roleTeams.get(role.role_id)?.size ?? 0; + break; + case RoleColumn.Teams: { + const uids = context.roleTeams.get(role.role_id); + row.teams = uids ? resolveSortedNames(uids, context.teamNameById) : []; + break; + } } - return row + } + return row; } function formatListCell(values: string[] | undefined): string { - return values?.length ? values.join('\n') : '' + return values?.length ? values.join("\n") : ""; } function formatCell(row: ListRoleRow, col: RoleColumn): string { - switch (col) { - case RoleColumn.VisibleBelow: - return row.visible_below == null ? '' : row.visible_below ? 'Yes' : 'No' - case RoleColumn.DefaultRole: - return row.default_role == null ? '' : row.default_role ? 'Yes' : 'No' - case RoleColumn.Admin: - return row.admin == null ? '' : row.admin ? 'Yes' : 'No' - case RoleColumn.Node: - return row.node ?? '' - case RoleColumn.UserCount: - return row.user_count == null ? '' : String(row.user_count) - case RoleColumn.Users: - return formatListCell(row.users) - case RoleColumn.TeamCount: - return row.team_count == null ? '' : String(row.team_count) - case RoleColumn.Teams: - return formatListCell(row.teams) - } + switch (col) { + case RoleColumn.VisibleBelow: + return row.visible_below == null ? "" : row.visible_below ? "Yes" : "No"; + case RoleColumn.DefaultRole: + return row.default_role == null ? "" : row.default_role ? "Yes" : "No"; + case RoleColumn.Admin: + return row.admin == null ? "" : row.admin ? "Yes" : "No"; + case RoleColumn.Node: + return row.node ?? ""; + case RoleColumn.UserCount: + return row.user_count == null ? "" : String(row.user_count); + case RoleColumn.Users: + return formatListCell(row.users); + case RoleColumn.TeamCount: + return row.team_count == null ? "" : String(row.team_count); + case RoleColumn.Teams: + return formatListCell(row.teams); + } } diff --git a/KeeperSdk/src/roles/roleTypes.ts b/KeeperSdk/src/roles/roleTypes.ts index 09f95e0c..a15e8990 100644 --- a/KeeperSdk/src/roles/roleTypes.ts +++ b/KeeperSdk/src/roles/roleTypes.ts @@ -1,53 +1,54 @@ -import type { EnterpriseDataManagerApi } from '../teams/enterpriseData' +import type { EnterpriseDataManagerApi } from "../teams/enterpriseData"; export enum RoleColumn { - VisibleBelow = 'visible_below', - DefaultRole = 'default_role', - Admin = 'admin', - Node = 'node', - UserCount = 'user_count', - Users = 'users', - TeamCount = 'team_count', - Teams = 'teams', + VisibleBelow = "visible_below", + DefaultRole = "default_role", + Admin = "admin", + Node = "node", + UserCount = "user_count", + Users = "users", + TeamCount = "team_count", + Teams = "teams", } -export type RoleColumnInput = RoleColumn | `${RoleColumn}` +export type RoleColumnInput = RoleColumn | `${RoleColumn}`; -export const SUPPORTED_ROLE_COLUMNS: readonly RoleColumn[] = Object.values(RoleColumn) +export const SUPPORTED_ROLE_COLUMNS: readonly RoleColumn[] = + Object.values(RoleColumn); export const DEFAULT_ROLE_COLUMNS: readonly RoleColumn[] = [ - RoleColumn.DefaultRole, - RoleColumn.Admin, - RoleColumn.Node, - RoleColumn.UserCount, -] + RoleColumn.DefaultRole, + RoleColumn.Admin, + RoleColumn.Node, + RoleColumn.UserCount, +]; -export const ALL_COLUMNS_WILDCARD = '*' as const +export const ALL_COLUMNS_WILDCARD = "*" as const; export type ListRolesOptions = { - pattern?: string | null - columns?: RoleColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null - enterpriseData?: EnterpriseDataManagerApi -} + pattern?: string | null; + columns?: RoleColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null; + enterpriseData?: EnterpriseDataManagerApi; +}; export type ListRoleRow = { - role_id: number - name: string - visible_below?: boolean - default_role?: boolean - admin?: boolean - node?: string - user_count?: number - users?: string[] - team_count?: number - teams?: string[] -} + role_id: number; + name: string; + visible_below?: boolean; + default_role?: boolean; + admin?: boolean; + node?: string; + user_count?: number; + users?: string[]; + team_count?: number; + teams?: string[]; +}; export type FormattedRolesTable = { - headers: string[] - rows: string[][] -} + headers: string[]; + rows: string[][]; +}; export type FormatRolesTableOptions = { - columns?: ListRolesOptions['columns'] -} + columns?: ListRolesOptions["columns"]; +}; diff --git a/KeeperSdk/src/roles/roleUtils.ts b/KeeperSdk/src/roles/roleUtils.ts index 5e3c5037..2843fbd2 100644 --- a/KeeperSdk/src/roles/roleUtils.ts +++ b/KeeperSdk/src/roles/roleUtils.ts @@ -1,229 +1,268 @@ import { - roleEnforcementAddCommand, - roleEnforcementRemoveCommand, - roleEnforcementUpdateCommand, - type Auth, - type KeeperResponse, -} from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' + roleEnforcementAddCommand, + roleEnforcementRemoveCommand, + roleEnforcementUpdateCommand, + type Auth, + type KeeperResponse, +} from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; import { - EnterpriseDataManager, - type EnterpriseNode, - type EnterpriseRole, - type EnterpriseRoleEnforcementLink, -} from '../teams/enterpriseData' + EnterpriseDataManager, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseRoleEnforcementLink, +} from "../teams/enterpriseData"; -export const ROLE_TABLE_HEADERS = ['#', 'Status', 'Role Name', 'Role ID', 'Node ID', 'Detail'] as const -export const NODE_PATH_SEPARATOR = '\\' +export const ROLE_TABLE_HEADERS = [ + "#", + "Status", + "Role Name", + "Role ID", + "Node ID", + "Detail", +] as const; +export const NODE_PATH_SEPARATOR = "\\"; -export type RoleToggle = 'on' | 'off' -export type RoleToggleInput = RoleToggle | `${RoleToggle}` | null | undefined -export type EnforcementPair = { key: string; value: string } +export type RoleToggle = "on" | "off"; +export type RoleToggleInput = RoleToggle | `${RoleToggle}` | null | undefined; +export type EnforcementPair = { key: string; value: string }; -type RoleResultRow = { status: string; roleName: string; roleId: number; nodeId: number } +type RoleResultRow = { + status: string; + roleName: string; + roleId: number; + nodeId: number; +}; -export function normalizeIdentifiers(values: ReadonlyArray | null | undefined): string[] { - return (values || []) - .map((value) => (typeof value === 'string' ? value.trim() : '')) - .filter((value) => value.length > 0) +export function normalizeIdentifiers( + values: ReadonlyArray | null | undefined, +): string[] { + return (values || []) + .map((value) => (typeof value === "string" ? value.trim() : "")) + .filter((value) => value.length > 0); } export function validateRoleName(name: string): void { - if (!name.trim()) { - throw new KeeperSdkError('Role name cannot be empty.', ResultCodes.ROLE_NAME_EMPTY) - } + if (!name.trim()) { + throw new KeeperSdkError( + "Role name cannot be empty.", + ResultCodes.ROLE_NAME_EMPTY, + ); + } } -export function buildRolesByLowerName(roles: EnterpriseRole[]): Map { - const map = new Map() - for (const role of roles) { - const key = (role.displayName || '').trim().toLowerCase() - if (!key) continue - const bucket = map.get(key) - if (bucket) bucket.push(role) - else map.set(key, [role]) - } - return map +export function buildRolesByLowerName( + roles: EnterpriseRole[], +): Map { + const map = new Map(); + for (const role of roles) { + const key = (role.displayName || "").trim().toLowerCase(); + if (!key) continue; + const bucket = map.get(key); + if (bucket) bucket.push(role); + else map.set(key, [role]); + } + return map; } -export function resolveExistingRoles(roles: EnterpriseRole[], identifiers: string[]): EnterpriseRole[] { - const byId = new Map(roles.map((role) => [role.role_id, role])) - const byLowerName = buildRolesByLowerName(roles) - - const found = new Map() - const missing: string[] = [] - for (const identifier of identifiers) { - const trimmed = identifier.trim() - const numeric = Number(trimmed) - if (Number.isInteger(numeric) && byId.has(numeric)) { - const match = byId.get(numeric)! - found.set(match.role_id, match) - continue - } - const nameMatches = byLowerName.get(trimmed.toLowerCase()) - if (!nameMatches || nameMatches.length === 0) { - missing.push(trimmed) - continue - } - if (nameMatches.length > 1) { - throw new KeeperSdkError( - `Role name "${trimmed}" is not unique. Use Role ID instead.`, - ResultCodes.MULTIPLE_ROLE_MATCHES - ) - } - found.set(nameMatches[0].role_id, nameMatches[0]) - } +export function resolveExistingRoles( + roles: EnterpriseRole[], + identifiers: string[], +): EnterpriseRole[] { + const byId = new Map( + roles.map((role) => [role.role_id, role]), + ); + const byLowerName = buildRolesByLowerName(roles); - if (missing.length > 0) { - throw new KeeperSdkError( - `Role(s) "${missing.join(', ')}" could not be resolved.`, - ResultCodes.ROLE_NOT_FOUND - ) + const found = new Map(); + const missing: string[] = []; + for (const identifier of identifiers) { + const trimmed = identifier.trim(); + const numeric = Number(trimmed); + if (Number.isInteger(numeric) && byId.has(numeric)) { + const match = byId.get(numeric)!; + found.set(match.role_id, match); + continue; + } + const nameMatches = byLowerName.get(trimmed.toLowerCase()); + if (!nameMatches || nameMatches.length === 0) { + missing.push(trimmed); + continue; } - return Array.from(found.values()) + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Role name "${trimmed}" is not unique. Use Role ID instead.`, + ResultCodes.MULTIPLE_ROLE_MATCHES, + ); + } + found.set(nameMatches[0].role_id, nameMatches[0]); + } + + if (missing.length > 0) { + throw new KeeperSdkError( + `Role(s) "${missing.join(", ")}" could not be resolved.`, + ResultCodes.ROLE_NOT_FOUND, + ); + } + return Array.from(found.values()); } export function parseEnforcements(rawList: string[]): EnforcementPair[] { - return rawList - .map((raw) => { - const sep = raw.indexOf(':') - if (sep < 1) return null - const key = raw.slice(0, sep).trim() - const value = raw.slice(sep + 1).trim() - return key && value ? ({ key, value } as EnforcementPair) : null - }) - .filter((entry): entry is EnforcementPair => entry !== null) + return rawList + .map((raw) => { + const sep = raw.indexOf(":"); + if (sep < 1) return null; + const key = raw.slice(0, sep).trim(); + const value = raw.slice(sep + 1).trim(); + return key && value ? ({ key, value } as EnforcementPair) : null; + }) + .filter((entry): entry is EnforcementPair => entry !== null); } export function resolveToggle(input: RoleToggleInput): boolean | undefined { - if (input === 'on') return true - if (input === 'off') return false - return undefined + if (input === "on") return true; + if (input === "off") return false; + return undefined; } -export function applyDecryptedRoleNames(roles: EnterpriseRole[], decrypted: Map): void { - if (decrypted.size === 0) return - for (const role of roles) { - const display = decrypted.get(role.role_id) - if (display) role.displayName = display - } +export function applyDecryptedRoleNames( + roles: EnterpriseRole[], + decrypted: Map, +): void { + if (decrypted.size === 0) return; + for (const role of roles) { + const display = decrypted.get(role.role_id); + if (display) role.displayName = display; + } } -export function nodePathOrFallback(nodes: EnterpriseNode[], node: EnterpriseNode): string { - const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { - omitRoot: false, - separator: NODE_PATH_SEPARATOR, - }) - return path || (node.displayName || '').trim() || String(node.node_id) +export function nodePathOrFallback( + nodes: EnterpriseNode[], + node: EnterpriseNode, +): string { + const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }); + return path || (node.displayName || "").trim() || String(node.node_id); } -export function assertCommandSucceeded(response: KeeperResponse, fallbackMessage: string, fallbackCode: string): void { - if ((response.result || '').toLowerCase() === 'fail') { - throw new KeeperSdkError( - response.message || response.result_code || fallbackMessage, - response.result_code || fallbackCode - ) - } +export function assertCommandSucceeded( + response: KeeperResponse, + fallbackMessage: string, + fallbackCode: string, +): void { + if ((response.result || "").toLowerCase() === "fail") { + throw new KeeperSdkError( + response.message || response.result_code || fallbackMessage, + response.result_code || fallbackCode, + ); + } } -const ENFORCEMENT_FALSE = new Set(['false', 'f', '0', 'off', 'no', 'n']) -const ENFORCEMENT_TRUE = new Set(['true', 't', '1', 'on', 'yes', 'y']) +const ENFORCEMENT_FALSE = new Set(["false", "f", "0", "off", "no", "n"]); +const ENFORCEMENT_TRUE = new Set(["true", "t", "1", "on", "yes", "y"]); function normalizeEnforcementKey(key: string): string { - return key.trim().toLowerCase().replace(/-/g, '_') + return key.trim().toLowerCase().replace(/-/g, "_"); } function roleHasEnforcement( - roleId: number, - enforcement: string, - links: readonly EnterpriseRoleEnforcementLink[] + roleId: number, + enforcement: string, + links: readonly EnterpriseRoleEnforcementLink[], ): boolean { - return links.some( - (link) => - link.role_id === roleId && normalizeEnforcementKey(link.enforcement_type) === enforcement - ) + return links.some( + (link) => + link.role_id === roleId && + normalizeEnforcementKey(link.enforcement_type) === enforcement, + ); } export async function applyRoleEnforcements( - auth: Auth, - roleId: number, - pairs: EnforcementPair[], - existingLinks: readonly EnterpriseRoleEnforcementLink[] = [] + auth: Auth, + roleId: number, + pairs: EnforcementPair[], + existingLinks: readonly EnterpriseRoleEnforcementLink[] = [], ): Promise { - for (const { key, value } of pairs) { - const enforcement = normalizeEnforcementKey(key) - const lower = value.trim().toLowerCase() - - if (ENFORCEMENT_FALSE.has(lower)) { - if (!roleHasEnforcement(roleId, enforcement, existingLinks)) continue - const response = await auth.executeRestCommand( - roleEnforcementRemoveCommand({ role_id: roleId, enforcement }) - ) - assertCommandSucceeded( - response, - `role_enforcement_remove failed: ${enforcement}`, - ResultCodes.ROLE_ENFORCEMENT_FAILED - ) - continue - } - - if (ENFORCEMENT_TRUE.has(lower)) { - const exists = roleHasEnforcement(roleId, enforcement, existingLinks) - const response = await auth.executeRestCommand( - exists - ? roleEnforcementUpdateCommand({ role_id: roleId, enforcement }) - : roleEnforcementAddCommand({ role_id: roleId, enforcement }) - ) - assertCommandSucceeded( - response, - `role_enforcement_${exists ? 'update' : 'add'} failed: ${enforcement}`, - ResultCodes.ROLE_ENFORCEMENT_FAILED - ) - continue - } - - const exists = roleHasEnforcement(roleId, enforcement, existingLinks) - const response = await auth.executeRestCommand( - exists - ? roleEnforcementUpdateCommand({ role_id: roleId, enforcement, value }) - : roleEnforcementAddCommand({ role_id: roleId, enforcement, value }) - ) - assertCommandSucceeded( - response, - `role_enforcement_${exists ? 'update' : 'add'} failed: ${enforcement}`, - ResultCodes.ROLE_ENFORCEMENT_FAILED - ) + for (const { key, value } of pairs) { + const enforcement = normalizeEnforcementKey(key); + const lower = value.trim().toLowerCase(); + + if (ENFORCEMENT_FALSE.has(lower)) { + if (!roleHasEnforcement(roleId, enforcement, existingLinks)) continue; + const response = await auth.executeRestCommand( + roleEnforcementRemoveCommand({ role_id: roleId, enforcement }), + ); + assertCommandSucceeded( + response, + `role_enforcement_remove failed: ${enforcement}`, + ResultCodes.ROLE_ENFORCEMENT_FAILED, + ); + continue; } + + if (ENFORCEMENT_TRUE.has(lower)) { + const exists = roleHasEnforcement(roleId, enforcement, existingLinks); + const response = await auth.executeRestCommand( + exists + ? roleEnforcementUpdateCommand({ role_id: roleId, enforcement }) + : roleEnforcementAddCommand({ role_id: roleId, enforcement }), + ); + assertCommandSucceeded( + response, + `role_enforcement_${exists ? "update" : "add"} failed: ${enforcement}`, + ResultCodes.ROLE_ENFORCEMENT_FAILED, + ); + continue; + } + + const exists = roleHasEnforcement(roleId, enforcement, existingLinks); + const response = await auth.executeRestCommand( + exists + ? roleEnforcementUpdateCommand({ role_id: roleId, enforcement, value }) + : roleEnforcementAddCommand({ role_id: roleId, enforcement, value }), + ); + assertCommandSucceeded( + response, + `role_enforcement_${exists ? "update" : "add"} failed: ${enforcement}`, + ResultCodes.ROLE_ENFORCEMENT_FAILED, + ); + } } export function buildRoleResultRows( - items: ReadonlyArray, - detailFor: (item: T) => string + items: ReadonlyArray, + detailFor: (item: T) => string, ): string[][] { - return items.map((item, index) => [ - String(index + 1), - item.status, - item.roleName, - String(item.roleId), - String(item.nodeId), - detailFor(item), - ]) + return items.map((item, index) => [ + String(index + 1), + item.status, + item.roleName, + String(item.roleId), + String(item.nodeId), + detailFor(item), + ]); } export function renderRoleResultTable( - headers: ReadonlyArray, - rows: string[][], - summary: string, - prefixLines: ReadonlyArray = [] + headers: ReadonlyArray, + rows: string[][], + summary: string, + prefixLines: ReadonlyArray = [], ): string { - const widths = headers.map((header, i) => Math.max(header.length, ...rows.map((row) => (row[i] || '').length))) - const pad = (cell: string, i: number) => cell + ' '.repeat(Math.max(0, widths[i] - cell.length)) - const formatRow = (cells: ReadonlyArray) => cells.map((cell, i) => pad(cell, i)).join(' ') - return [ - ...prefixLines, - formatRow(headers), - formatRow(widths.map((width) => '-'.repeat(width))), - ...rows.map(formatRow), - summary, - ].join('\n') + const widths = headers.map((header, i) => + Math.max(header.length, ...rows.map((row) => (row[i] || "").length)), + ); + const pad = (cell: string, i: number) => + cell + " ".repeat(Math.max(0, widths[i] - cell.length)); + const formatRow = (cells: ReadonlyArray) => + cells.map((cell, i) => pad(cell, i)).join(" "); + return [ + ...prefixLines, + formatRow(headers), + formatRow(widths.map((width) => "-".repeat(width))), + ...rows.map(formatRow), + summary, + ].join("\n"); } diff --git a/KeeperSdk/src/roles/updateRole.ts b/KeeperSdk/src/roles/updateRole.ts index d0fc5679..6b3918ef 100644 --- a/KeeperSdk/src/roles/updateRole.ts +++ b/KeeperSdk/src/roles/updateRole.ts @@ -1,173 +1,229 @@ import { - encryptObjectForStorage, - roleUpdateCommand, - type Auth, - type RoleEditRequest, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' -import { EnterpriseDataInclude, EnterpriseDataManager, type EnterpriseRole } from '../teams/enterpriseData' + encryptObjectForStorage, + roleUpdateCommand, + type Auth, + type RoleEditRequest, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; import { - applyDecryptedNodeNames, - applyEnterpriseNameToRoot, - parentNeedsNameLookup, - resolveParentNode, -} from '../teams/teamUtils' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseRole, +} from "../teams/enterpriseData"; import { - applyDecryptedRoleNames, - applyRoleEnforcements, - assertCommandSucceeded, - buildRoleResultRows, - normalizeIdentifiers, - parseEnforcements, - renderRoleResultTable, - resolveExistingRoles, - resolveToggle, - ROLE_TABLE_HEADERS, - type RoleToggleInput, -} from './roleUtils' + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveParentNode, +} from "../teams/teamUtils"; +import { + applyDecryptedRoleNames, + applyRoleEnforcements, + assertCommandSucceeded, + buildRoleResultRows, + normalizeIdentifiers, + parseEnforcements, + renderRoleResultTable, + resolveExistingRoles, + resolveToggle, + ROLE_TABLE_HEADERS, + type RoleToggleInput, +} from "./roleUtils"; const UPDATE_ROLE_BASE_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Roles, -] + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Roles, +]; export enum UpdateRoleStatus { - Updated = 'updated', - Failed = 'failed', + Updated = "updated", + Failed = "failed", } export type UpdateRoleInput = { - roles: string[] - name?: string - parent?: string | number | null - newUser?: RoleToggleInput - visibleBelow?: RoleToggleInput - enforcements?: string[] -} + roles: string[]; + name?: string; + parent?: string | number | null; + newUser?: RoleToggleInput; + visibleBelow?: RoleToggleInput; + enforcements?: string[]; +}; export type UpdateRoleItemResult = { - roleId: number - roleName: string - nodeId: number - status: UpdateRoleStatus - message?: string -} + roleId: number; + roleName: string; + nodeId: number; + status: UpdateRoleStatus; + message?: string; +}; export type UpdateRoleResult = { - success: boolean - items: UpdateRoleItemResult[] - updated: number - failed: number -} + success: boolean; + items: UpdateRoleItemResult[]; + updated: number; + failed: number; +}; export type FormattedUpdateRoleTable = { - headers: string[] - rows: string[][] - summary: string -} - -export async function updateRoles(auth: Auth, input: UpdateRoleInput): Promise { - const identifiers = normalizeIdentifiers(input.roles) - if (identifiers.length === 0) { - throw new KeeperSdkError('No roles to update.', ResultCodes.NO_ROLES_TO_UPDATE) - } - - const newName = input.name?.trim() || undefined - if (newName && identifiers.length > 1) { - throw new KeeperSdkError( - 'Cannot rename more than one role in a single update.', - ResultCodes.ROLE_RENAME_MULTI_NOT_ALLOWED - ) - } - - const newUserInherit = resolveToggle(input.newUser) - const visibleBelow = resolveToggle(input.visibleBelow) - const enforcements = parseEnforcements(input.enforcements ?? []) - const includes = [...UPDATE_ROLE_BASE_INCLUDES] - if (enforcements.length > 0) includes.push(EnterpriseDataInclude.RoleEnforcements) - - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(includes), - enterpriseData.getDisplayNames(), - ]) - - const nodes = response.nodes || [] - applyDecryptedNodeNames(nodes, displayNames.nodes) - applyEnterpriseNameToRoot(nodes, response.enterprise_name) - if (parentNeedsNameLookup(input.parent ?? null)) await enterpriseData.decryptNodeNames(nodes) - - const roles = response.roles || [] - applyDecryptedRoleNames(roles, displayNames.roles) - const resolvedRoles = resolveExistingRoles(roles, identifiers) - - let overrideNodeId: number | null = null - if (input.parent !== undefined && input.parent !== null && input.parent !== '') { - overrideNodeId = resolveParentNode(nodes, input.parent).node_id + headers: string[]; + rows: string[][]; + summary: string; +}; + +export async function updateRoles( + auth: Auth, + input: UpdateRoleInput, +): Promise { + const identifiers = normalizeIdentifiers(input.roles); + if (identifiers.length === 0) { + throw new KeeperSdkError( + "No roles to update.", + ResultCodes.NO_ROLES_TO_UPDATE, + ); + } + + const newName = input.name?.trim() || undefined; + if (newName && identifiers.length > 1) { + throw new KeeperSdkError( + "Cannot rename more than one role in a single update.", + ResultCodes.ROLE_RENAME_MULTI_NOT_ALLOWED, + ); + } + + const newUserInherit = resolveToggle(input.newUser); + const visibleBelow = resolveToggle(input.visibleBelow); + const enforcements = parseEnforcements(input.enforcements ?? []); + const includes = [...UPDATE_ROLE_BASE_INCLUDES]; + if (enforcements.length > 0) + includes.push(EnterpriseDataInclude.RoleEnforcements); + + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(includes), + enterpriseData.getDisplayNames(), + ]); + + const nodes = response.nodes || []; + applyDecryptedNodeNames(nodes, displayNames.nodes); + applyEnterpriseNameToRoot(nodes, response.enterprise_name); + if (parentNeedsNameLookup(input.parent ?? null)) + await enterpriseData.decryptNodeNames(nodes); + + const roles = response.roles || []; + applyDecryptedRoleNames(roles, displayNames.roles); + const resolvedRoles = resolveExistingRoles(roles, identifiers); + + let overrideNodeId: number | null = null; + if ( + input.parent !== undefined && + input.parent !== null && + input.parent !== "" + ) { + overrideNodeId = resolveParentNode(nodes, input.parent).node_id; + } + + const needsTreeKey = + Boolean(newName) || resolvedRoles.some((role) => !role.encrypted_data); + const treeKey = needsTreeKey ? await enterpriseData.getTreeKey() : null; + + const items: UpdateRoleItemResult[] = []; + for (const role of resolvedRoles) { + const targetNodeId = overrideNodeId ?? role.node_id ?? 0; + const currentName = (role.displayName || "").trim() || String(role.role_id); + + try { + const payload: RoleEditRequest = { + role_id: role.role_id, + node_id: targetNodeId, + encrypted_data: await resolveEncryptedData( + role, + newName, + currentName, + treeKey, + ), + visible_below: visibleBelow ?? role.visible_below ?? false, + new_user_inherit: newUserInherit ?? role.new_user_inherit ?? false, + }; + await sendRoleUpdate(auth, payload); + await applyRoleEnforcements( + auth, + role.role_id, + enforcements, + response.role_enforcements || [], + ); + items.push({ + roleId: role.role_id, + roleName: newName || currentName, + nodeId: targetNodeId, + status: UpdateRoleStatus.Updated, + }); + } catch (err) { + items.push({ + roleId: role.role_id, + roleName: currentName, + nodeId: role.node_id ?? 0, + status: UpdateRoleStatus.Failed, + message: extractErrorMessage(err), + }); } + } - const needsTreeKey = Boolean(newName) || resolvedRoles.some((role) => !role.encrypted_data) - const treeKey = needsTreeKey ? await enterpriseData.getTreeKey() : null - - const items: UpdateRoleItemResult[] = [] - for (const role of resolvedRoles) { - const targetNodeId = overrideNodeId ?? role.node_id ?? 0 - const currentName = (role.displayName || '').trim() || String(role.role_id) - - try { - const payload: RoleEditRequest = { - role_id: role.role_id, - node_id: targetNodeId, - encrypted_data: await resolveEncryptedData(role, newName, currentName, treeKey), - visible_below: visibleBelow ?? role.visible_below ?? false, - new_user_inherit: newUserInherit ?? role.new_user_inherit ?? false, - } - await sendRoleUpdate(auth, payload) - await applyRoleEnforcements(auth, role.role_id, enforcements, response.role_enforcements || []) - items.push({ roleId: role.role_id, roleName: newName || currentName, nodeId: targetNodeId, status: UpdateRoleStatus.Updated }) - } catch (err) { - items.push({ roleId: role.role_id, roleName: currentName, nodeId: role.node_id ?? 0, status: UpdateRoleStatus.Failed, message: extractErrorMessage(err) }) - } - } - - return finalizeResult(items) + return finalizeResult(items); } -export function formatUpdateRoleResult(result: UpdateRoleResult): FormattedUpdateRoleTable { - return { - headers: [...ROLE_TABLE_HEADERS], - rows: buildRoleResultRows(result.items, (item) => item.message || ''), - summary: `Updated: ${result.updated} Failed: ${result.failed}`, - } +export function formatUpdateRoleResult( + result: UpdateRoleResult, +): FormattedUpdateRoleTable { + return { + headers: [...ROLE_TABLE_HEADERS], + rows: buildRoleResultRows(result.items, (item) => item.message || ""), + summary: `Updated: ${result.updated} Failed: ${result.failed}`, + }; } -export function renderUpdateRoleAsciiTable(table: FormattedUpdateRoleTable): string { - return renderRoleResultTable(table.headers, table.rows, table.summary) +export function renderUpdateRoleAsciiTable( + table: FormattedUpdateRoleTable, +): string { + return renderRoleResultTable(table.headers, table.rows, table.summary); } async function resolveEncryptedData( - role: EnterpriseRole, - newName: string | undefined, - currentName: string, - treeKey: Uint8Array | null + role: EnterpriseRole, + newName: string | undefined, + currentName: string, + treeKey: Uint8Array | null, ): Promise { - if (!newName && role.encrypted_data) return role.encrypted_data - if (!treeKey) { - throw new KeeperSdkError( - `Enterprise tree key is unavailable; cannot ${newName ? 'rename role' : 'preserve role name'}.`, - ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE - ) - } - return encryptObjectForStorage({ displayname: newName || currentName }, treeKey) + if (!newName && role.encrypted_data) return role.encrypted_data; + if (!treeKey) { + throw new KeeperSdkError( + `Enterprise tree key is unavailable; cannot ${newName ? "rename role" : "preserve role name"}.`, + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE, + ); + } + return encryptObjectForStorage( + { displayname: newName || currentName }, + treeKey, + ); } -async function sendRoleUpdate(auth: Auth, payload: RoleEditRequest): Promise { - const response = await auth.executeRestCommand(roleUpdateCommand(payload)) - assertCommandSucceeded(response, `role_update failed for role_id=${payload.role_id}`, ResultCodes.ROLE_UPDATE_FAILED) +async function sendRoleUpdate( + auth: Auth, + payload: RoleEditRequest, +): Promise { + const response = await auth.executeRestCommand(roleUpdateCommand(payload)); + assertCommandSucceeded( + response, + `role_update failed for role_id=${payload.role_id}`, + ResultCodes.ROLE_UPDATE_FAILED, + ); } function finalizeResult(items: UpdateRoleItemResult[]): UpdateRoleResult { - const updated = items.filter((item) => item.status === UpdateRoleStatus.Updated).length - const failed = items.filter((item) => item.status === UpdateRoleStatus.Failed).length - return { success: failed === 0 && updated > 0, items, updated, failed } + const updated = items.filter( + (item) => item.status === UpdateRoleStatus.Updated, + ).length; + const failed = items.filter( + (item) => item.status === UpdateRoleStatus.Failed, + ).length; + return { success: failed === 0 && updated > 0, items, updated, failed }; } diff --git a/KeeperSdk/src/roles/viewRole.ts b/KeeperSdk/src/roles/viewRole.ts index e0aa336d..8edef129 100644 --- a/KeeperSdk/src/roles/viewRole.ts +++ b/KeeperSdk/src/roles/viewRole.ts @@ -1,448 +1,520 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseDisplayNames, - type EnterpriseNode, - type EnterpriseRole, - type EnterpriseRoleEnforcementLink, - type EnterpriseRoleManagedNodeLink, - type EnterpriseRolePrivilegeLink, - type EnterpriseRoleTeamLink, - type EnterpriseRoleUserLink, - type EnterpriseTeamRecord, - type EnterpriseUser, -} from '../teams/enterpriseData' - -const NODE_PATH_SEPARATOR = '\\' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseRoleEnforcementLink, + type EnterpriseRoleManagedNodeLink, + type EnterpriseRolePrivilegeLink, + type EnterpriseRoleTeamLink, + type EnterpriseRoleUserLink, + type EnterpriseTeamRecord, + type EnterpriseUser, +} from "../teams/enterpriseData"; + +const NODE_PATH_SEPARATOR = "\\"; const VIEW_ROLE_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Roles, - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Teams, - EnterpriseDataInclude.Users, - EnterpriseDataInclude.RoleUsers, - EnterpriseDataInclude.RoleTeams, - EnterpriseDataInclude.RolePrivileges, - EnterpriseDataInclude.ManagedNodes, - EnterpriseDataInclude.RoleEnforcements, -] + EnterpriseDataInclude.Roles, + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.Users, + EnterpriseDataInclude.RoleUsers, + EnterpriseDataInclude.RoleTeams, + EnterpriseDataInclude.RolePrivileges, + EnterpriseDataInclude.ManagedNodes, + EnterpriseDataInclude.RoleEnforcements, +]; export type RoleTeamInfo = { - team_uid: string - team_name: string -} + team_uid: string; + team_name: string; +}; export type RoleUserInfo = { - enterprise_user_id: number - username: string -} + enterprise_user_id: number; + username: string; +}; export type RoleManagedNodeInfo = { - node_id: number - node_name: string - cascade: boolean - privileges: string[] -} + node_id: number; + node_name: string; + cascade: boolean; + privileges: string[]; +}; export type RoleEnforcementInfo = { - name: string - value: string -} + name: string; + value: string; +}; export type RoleView = { - role_id: number - role_name: string - node_id: number - node_name: string - default_role: boolean - visible_below: boolean - role_teams?: RoleTeamInfo[] - role_users?: RoleUserInfo[] - managed_nodes?: RoleManagedNodeInfo[] - role_enforcements: RoleEnforcementInfo[] -} + role_id: number; + role_name: string; + node_id: number; + node_name: string; + default_role: boolean; + visible_below: boolean; + role_teams?: RoleTeamInfo[]; + role_users?: RoleUserInfo[]; + managed_nodes?: RoleManagedNodeInfo[]; + role_enforcements: RoleEnforcementInfo[]; +}; export type FormatRoleViewOptions = { - verbose?: boolean -} + verbose?: boolean; +}; export type RoleViewTableRow = { - label: string - value: string | string[] - id?: number | number[] -} + label: string; + value: string | string[]; + id?: number | number[]; +}; export type FormattedRoleViewTable = { - mainRows: RoleViewTableRow[] - enforcementRows: RoleEnforcementInfo[] - managedNodeTable: FormattedManagedNodePrivilegeTable | null - hasIdColumn: boolean -} + mainRows: RoleViewTableRow[]; + enforcementRows: RoleEnforcementInfo[]; + managedNodeTable: FormattedManagedNodePrivilegeTable | null; + hasIdColumn: boolean; +}; export type FormattedManagedNodePrivilegeTable = { - nodeHeaders: string[] - privilegeRows: Array<{ privilege: string; granted: boolean[] }> - cascadeRow: boolean[] -} - -export async function viewRole(auth: Auth, identifier: string): Promise { - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(VIEW_ROLE_INCLUDES), - enterpriseData.getDisplayNames(), - ]) - - const role = resolveRole(response.roles || [], displayNames.roles, identifier) - const nodePath = resolveNodePath(response.nodes || [], displayNames, role.node_id ?? 0) - - const roleTeams = buildRoleTeams(response.role_teams || [], response.teams || [], role.role_id) - const roleUsers = buildRoleUsers(response.role_users || [], response.users || [], role.role_id) - const managedNodes = buildManagedNodes( - response.managed_nodes || [], - response.role_privileges || [], - response.nodes || [], - displayNames, - role.role_id, - response.enterprise_name - ) - const enforcements = buildEnforcements(response.role_enforcements || [], role.role_id) - - const view: RoleView = { - role_id: role.role_id, - role_name: (role.displayName || '').trim() || String(role.role_id), - node_id: role.node_id ?? 0, - node_name: nodePath, - default_role: role.new_user_inherit ?? false, - visible_below: role.visible_below ?? false, - role_enforcements: enforcements, - } - if (roleTeams.length > 0) view.role_teams = roleTeams - if (roleUsers.length > 0) view.role_users = roleUsers - if (managedNodes.length > 0) view.managed_nodes = managedNodes - - return view + nodeHeaders: string[]; + privilegeRows: Array<{ privilege: string; granted: boolean[] }>; + cascadeRow: boolean[]; +}; + +export async function viewRole( + auth: Auth, + identifier: string, +): Promise { + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(VIEW_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]); + + const role = resolveRole( + response.roles || [], + displayNames.roles, + identifier, + ); + const nodePath = resolveNodePath( + response.nodes || [], + displayNames, + role.node_id ?? 0, + ); + + const roleTeams = buildRoleTeams( + response.role_teams || [], + response.teams || [], + role.role_id, + ); + const roleUsers = buildRoleUsers( + response.role_users || [], + response.users || [], + role.role_id, + ); + const managedNodes = buildManagedNodes( + response.managed_nodes || [], + response.role_privileges || [], + response.nodes || [], + displayNames, + role.role_id, + response.enterprise_name, + ); + const enforcements = buildEnforcements( + response.role_enforcements || [], + role.role_id, + ); + + const view: RoleView = { + role_id: role.role_id, + role_name: (role.displayName || "").trim() || String(role.role_id), + node_id: role.node_id ?? 0, + node_name: nodePath, + default_role: role.new_user_inherit ?? false, + visible_below: role.visible_below ?? false, + role_enforcements: enforcements, + }; + if (roleTeams.length > 0) view.role_teams = roleTeams; + if (roleUsers.length > 0) view.role_users = roleUsers; + if (managedNodes.length > 0) view.managed_nodes = managedNodes; + + return view; } export function formatRoleView( - view: RoleView, - options: FormatRoleViewOptions = {} + view: RoleView, + options: FormatRoleViewOptions = {}, ): FormattedRoleViewTable { - const verbose = options.verbose === true - - const mainRows: RoleViewTableRow[] = [ - { label: 'Role ID', value: String(view.role_id) }, - { label: 'Role Name', value: view.role_name }, - { - label: 'Node Name', - value: view.node_name, - id: verbose ? view.node_id : undefined, - }, - { label: 'Default Role', value: boolText(view.default_role) }, - { label: 'Visible Below', value: boolText(view.visible_below) }, - ] - - if (view.role_users && view.role_users.length > 0) { - mainRows.push({ - label: 'User(s)', - value: view.role_users.map((u) => u.username), - id: verbose ? view.role_users.map((u) => u.enterprise_user_id) : undefined, - }) - } - - if (view.role_teams && view.role_teams.length > 0) { - mainRows.push({ - label: 'Team(s)', - value: view.role_teams.map((t) => t.team_name), - }) - } - - const managedNodeTable = buildFormattedManagedNodeTable(view.managed_nodes ?? []) - - return { - mainRows, - enforcementRows: view.role_enforcements, - managedNodeTable, - hasIdColumn: verbose, - } + const verbose = options.verbose === true; + + const mainRows: RoleViewTableRow[] = [ + { label: "Role ID", value: String(view.role_id) }, + { label: "Role Name", value: view.role_name }, + { + label: "Node Name", + value: view.node_name, + id: verbose ? view.node_id : undefined, + }, + { label: "Default Role", value: boolText(view.default_role) }, + { label: "Visible Below", value: boolText(view.visible_below) }, + ]; + + if (view.role_users && view.role_users.length > 0) { + mainRows.push({ + label: "User(s)", + value: view.role_users.map((u) => u.username), + id: verbose + ? view.role_users.map((u) => u.enterprise_user_id) + : undefined, + }); + } + + if (view.role_teams && view.role_teams.length > 0) { + mainRows.push({ + label: "Team(s)", + value: view.role_teams.map((t) => t.team_name), + }); + } + + const managedNodeTable = buildFormattedManagedNodeTable( + view.managed_nodes ?? [], + ); + + return { + mainRows, + enforcementRows: view.role_enforcements, + managedNodeTable, + hasIdColumn: verbose, + }; } export function roleViewTable(table: FormattedRoleViewTable): string { - const sections: string[] = [] + const sections: string[] = []; - sections.push(renderMainSection(table)) + sections.push(renderMainSection(table)); - sections.push(renderEnforcementsSection(table.enforcementRows)) + sections.push(renderEnforcementsSection(table.enforcementRows)); - if (table.managedNodeTable) { - sections.push(renderManagedNodesSection(table.managedNodeTable)) - } + if (table.managedNodeTable) { + sections.push(renderManagedNodesSection(table.managedNodeTable)); + } - return sections.join('\n\n') + return sections.join("\n\n"); } function resolveRole( - roles: EnterpriseRole[], - decryptedNames: Map, - identifier: string + roles: EnterpriseRole[], + decryptedNames: Map, + identifier: string, ): EnterpriseRole { - const trimmed = (identifier ?? '').trim() - if (!trimmed) { - throw new KeeperSdkError('Role name or ID is required.', ResultCodes.ROLE_REQUIRED) - } - - for (const role of roles) { - const display = decryptedNames.get(role.role_id) - if (display) role.displayName = display - } - - const numeric = Number(trimmed) - if (Number.isFinite(numeric) && Number.isInteger(numeric)) { - const byId = roles.find((r) => r.role_id === numeric) - if (byId) return byId - } - - const lowered = trimmed.toLowerCase() - const nameMatches = roles.filter( - (r) => (r.displayName || '').trim().toLowerCase() === lowered - ) - if (nameMatches.length === 1) return nameMatches[0] - if (nameMatches.length > 1) { - throw new KeeperSdkError( - `Multiple roles match name "${trimmed}". Specify the role ID instead.`, - ResultCodes.MULTIPLE_ROLE_MATCHES - ) - } - throw new KeeperSdkError(`Role "${trimmed}" does not exist.`, ResultCodes.ROLE_NOT_FOUND) + const trimmed = (identifier ?? "").trim(); + if (!trimmed) { + throw new KeeperSdkError( + "Role name or ID is required.", + ResultCodes.ROLE_REQUIRED, + ); + } + + for (const role of roles) { + const display = decryptedNames.get(role.role_id); + if (display) role.displayName = display; + } + + const numeric = Number(trimmed); + if (Number.isFinite(numeric) && Number.isInteger(numeric)) { + const byId = roles.find((r) => r.role_id === numeric); + if (byId) return byId; + } + + const lowered = trimmed.toLowerCase(); + const nameMatches = roles.filter( + (r) => (r.displayName || "").trim().toLowerCase() === lowered, + ); + if (nameMatches.length === 1) return nameMatches[0]; + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Multiple roles match name "${trimmed}". Specify the role ID instead.`, + ResultCodes.MULTIPLE_ROLE_MATCHES, + ); + } + throw new KeeperSdkError( + `Role "${trimmed}" does not exist.`, + ResultCodes.ROLE_NOT_FOUND, + ); } function resolveNodePath( - nodes: EnterpriseNode[], - displayNames: EnterpriseDisplayNames, - nodeId: number + nodes: EnterpriseNode[], + displayNames: EnterpriseDisplayNames, + nodeId: number, ): string { - for (const node of nodes) { - const display = displayNames.nodes.get(node.node_id) - if (display) node.displayName = display - } - return EnterpriseDataManager.getNodePath(nodes, nodeId, { - omitRoot: false, - separator: NODE_PATH_SEPARATOR, - }) + for (const node of nodes) { + const display = displayNames.nodes.get(node.node_id); + if (display) node.displayName = display; + } + return EnterpriseDataManager.getNodePath(nodes, nodeId, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }); } function buildRoleTeams( - roleTeamLinks: EnterpriseRoleTeamLink[], - teams: EnterpriseTeamRecord[], - roleId: number + roleTeamLinks: EnterpriseRoleTeamLink[], + teams: EnterpriseTeamRecord[], + roleId: number, ): RoleTeamInfo[] { - const teamByUid = new Map() - for (const team of teams) teamByUid.set(team.team_uid, team) - - const result: RoleTeamInfo[] = [] - for (const link of roleTeamLinks) { - if (link.role_id !== roleId) continue - const team = teamByUid.get(link.team_uid) - if (team) result.push({ team_uid: team.team_uid, team_name: (team.name || team.team_uid).trim() }) - } - result.sort((a, b) => a.team_name.localeCompare(b.team_name, undefined, { sensitivity: 'base' })) - return result + const teamByUid = new Map(); + for (const team of teams) teamByUid.set(team.team_uid, team); + + const result: RoleTeamInfo[] = []; + for (const link of roleTeamLinks) { + if (link.role_id !== roleId) continue; + const team = teamByUid.get(link.team_uid); + if (team) + result.push({ + team_uid: team.team_uid, + team_name: (team.name || team.team_uid).trim(), + }); + } + result.sort((a, b) => + a.team_name.localeCompare(b.team_name, undefined, { sensitivity: "base" }), + ); + return result; } function buildRoleUsers( - roleUserLinks: EnterpriseRoleUserLink[], - users: EnterpriseUser[], - roleId: number + roleUserLinks: EnterpriseRoleUserLink[], + users: EnterpriseUser[], + roleId: number, ): RoleUserInfo[] { - const userById = new Map() - for (const user of users) userById.set(user.enterprise_user_id, user) - - const result: RoleUserInfo[] = [] - for (const link of roleUserLinks) { - if (link.role_id !== roleId) continue - const user = userById.get(link.enterprise_user_id) - if (user?.username) { - result.push({ enterprise_user_id: link.enterprise_user_id, username: user.username }) - } + const userById = new Map(); + for (const user of users) userById.set(user.enterprise_user_id, user); + + const result: RoleUserInfo[] = []; + for (const link of roleUserLinks) { + if (link.role_id !== roleId) continue; + const user = userById.get(link.enterprise_user_id); + if (user?.username) { + result.push({ + enterprise_user_id: link.enterprise_user_id, + username: user.username, + }); } - result.sort((a, b) => a.username.localeCompare(b.username, undefined, { sensitivity: 'base' })) - return result + } + result.sort((a, b) => + a.username.localeCompare(b.username, undefined, { sensitivity: "base" }), + ); + return result; } function buildManagedNodes( - managedNodeLinks: EnterpriseRoleManagedNodeLink[], - privileges: EnterpriseRolePrivilegeLink[], - nodes: EnterpriseNode[], - displayNames: EnterpriseDisplayNames, - roleId: number, - enterpriseName?: string + managedNodeLinks: EnterpriseRoleManagedNodeLink[], + privileges: EnterpriseRolePrivilegeLink[], + nodes: EnterpriseNode[], + displayNames: EnterpriseDisplayNames, + roleId: number, + enterpriseName?: string, ): RoleManagedNodeInfo[] { - const nodeById = new Map() - for (const node of nodes) { - const display = displayNames.nodes.get(node.node_id) - if (display) node.displayName = display - nodeById.set(node.node_id, node) - } - - const privilegesByNode = new Map() - for (const p of privileges) { - if (p.role_id !== roleId || !p.privilege_type) continue - const list = privilegesByNode.get(p.managed_node_id) - if (list) list.push(p.privilege_type) - else privilegesByNode.set(p.managed_node_id, [p.privilege_type]) - } - - const result: RoleManagedNodeInfo[] = [] - for (const link of managedNodeLinks) { - if (link.role_id !== roleId) continue - const node = nodeById.get(link.managed_node_id) - let nodeName = (node?.displayName || '').trim() - if (!nodeName && node && !node.parent_id && enterpriseName) nodeName = enterpriseName - if (!nodeName) nodeName = String(link.managed_node_id) - const nodePrivileges = (privilegesByNode.get(link.managed_node_id) ?? []).sort() - result.push({ - node_id: link.managed_node_id, - node_name: nodeName, - cascade: link.cascade_node_management, - privileges: nodePrivileges, - }) - } - result.sort((a, b) => a.node_name.localeCompare(b.node_name, undefined, { sensitivity: 'base' })) - return result + const nodeById = new Map(); + for (const node of nodes) { + const display = displayNames.nodes.get(node.node_id); + if (display) node.displayName = display; + nodeById.set(node.node_id, node); + } + + const privilegesByNode = new Map(); + for (const p of privileges) { + if (p.role_id !== roleId || !p.privilege_type) continue; + const list = privilegesByNode.get(p.managed_node_id); + if (list) list.push(p.privilege_type); + else privilegesByNode.set(p.managed_node_id, [p.privilege_type]); + } + + const result: RoleManagedNodeInfo[] = []; + for (const link of managedNodeLinks) { + if (link.role_id !== roleId) continue; + const node = nodeById.get(link.managed_node_id); + let nodeName = (node?.displayName || "").trim(); + if (!nodeName && node && !node.parent_id && enterpriseName) + nodeName = enterpriseName; + if (!nodeName) nodeName = String(link.managed_node_id); + const nodePrivileges = ( + privilegesByNode.get(link.managed_node_id) ?? [] + ).sort(); + result.push({ + node_id: link.managed_node_id, + node_name: nodeName, + cascade: link.cascade_node_management, + privileges: nodePrivileges, + }); + } + result.sort((a, b) => + a.node_name.localeCompare(b.node_name, undefined, { sensitivity: "base" }), + ); + return result; } function buildEnforcements( - links: EnterpriseRoleEnforcementLink[], - roleId: number + links: EnterpriseRoleEnforcementLink[], + roleId: number, ): RoleEnforcementInfo[] { - const result: RoleEnforcementInfo[] = [] - for (const link of links) { - if (link.role_id !== roleId || !link.value) continue - result.push({ name: link.enforcement_type, value: link.value }) - } - result.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) - return result + const result: RoleEnforcementInfo[] = []; + for (const link of links) { + if (link.role_id !== roleId || !link.value) continue; + result.push({ name: link.enforcement_type, value: link.value }); + } + result.sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); + return result; } function buildFormattedManagedNodeTable( - managedNodes: RoleManagedNodeInfo[] + managedNodes: RoleManagedNodeInfo[], ): FormattedManagedNodePrivilegeTable | null { - if (managedNodes.length === 0) return null - - const allPrivileges = new Set() - for (const mn of managedNodes) { - for (const p of mn.privileges) allPrivileges.add(p) - } - - const privileges = [...allPrivileges].sort() - const privilegeRows = privileges.map((privilege) => ({ - privilege, - granted: managedNodes.map((mn) => mn.privileges.includes(privilege)), - })) - - return { - nodeHeaders: managedNodes.map((mn) => mn.node_name), - privilegeRows, - cascadeRow: managedNodes.map((mn) => mn.cascade), - } + if (managedNodes.length === 0) return null; + + const allPrivileges = new Set(); + for (const mn of managedNodes) { + for (const p of mn.privileges) allPrivileges.add(p); + } + + const privileges = [...allPrivileges].sort(); + const privilegeRows = privileges.map((privilege) => ({ + privilege, + granted: managedNodes.map((mn) => mn.privileges.includes(privilege)), + })); + + return { + nodeHeaders: managedNodes.map((mn) => mn.node_name), + privilegeRows, + cascadeRow: managedNodes.map((mn) => mn.cascade), + }; } function renderMainSection(table: FormattedRoleViewTable): string { - const { mainRows, hasIdColumn } = table - - const labelWidth = mainRows.reduce((max, row) => Math.max(max, row.label.length), 0) - const expandedRows = mainRows.map((row) => ({ - label: row.label, - valueLines: asLines(row.value), - idLines: hasIdColumn ? asIdLines(row.id) : [], - })) - - const valueWidth = expandedRows.reduce( - (max, row) => Math.max(max, ...row.valueLines.map((l) => l.length)), - 0 - ) - const idWidth = hasIdColumn - ? expandedRows.reduce((max, row) => Math.max(max, ...row.idLines.map((l) => l.length)), 0) - : 0 - - const columnWidths = hasIdColumn ? [labelWidth, valueWidth, idWidth] : [labelWidth, valueWidth] - const lines: string[] = [] - for (const row of expandedRows) { - const lineCount = Math.max(row.valueLines.length, hasIdColumn ? row.idLines.length : 0, 1) - for (let li = 0; li < lineCount; li++) { - const cells = [ - li === 0 ? row.label : '', - row.valueLines[li] ?? '', - ...(hasIdColumn ? [row.idLines[li] ?? ''] : []), - ] - lines.push(formatAsciiRow(cells, columnWidths)) - } + const { mainRows, hasIdColumn } = table; + + const labelWidth = mainRows.reduce( + (max, row) => Math.max(max, row.label.length), + 0, + ); + const expandedRows = mainRows.map((row) => ({ + label: row.label, + valueLines: asLines(row.value), + idLines: hasIdColumn ? asIdLines(row.id) : [], + })); + + const valueWidth = expandedRows.reduce( + (max, row) => Math.max(max, ...row.valueLines.map((l) => l.length)), + 0, + ); + const idWidth = hasIdColumn + ? expandedRows.reduce( + (max, row) => Math.max(max, ...row.idLines.map((l) => l.length)), + 0, + ) + : 0; + + const columnWidths = hasIdColumn + ? [labelWidth, valueWidth, idWidth] + : [labelWidth, valueWidth]; + const lines: string[] = []; + for (const row of expandedRows) { + const lineCount = Math.max( + row.valueLines.length, + hasIdColumn ? row.idLines.length : 0, + 1, + ); + for (let li = 0; li < lineCount; li++) { + const cells = [ + li === 0 ? row.label : "", + row.valueLines[li] ?? "", + ...(hasIdColumn ? [row.idLines[li] ?? ""] : []), + ]; + lines.push(formatAsciiRow(cells, columnWidths)); } - return lines.join('\n') + } + return lines.join("\n"); } function renderEnforcementsSection(rows: RoleEnforcementInfo[]): string { - const title = 'Role Enforcements' - if (rows.length === 0) return `${title}\n (none)` - - const nameWidth = Math.max('Name'.length, ...rows.map((r) => r.name.length)) - const valWidth = Math.max('Value'.length, ...rows.map((r) => r.value.length)) - const columnWidths = [nameWidth, valWidth] - - const lines: string[] = [ - title, - formatAsciiRow(['Name', 'Value'], columnWidths), - formatAsciiRow(['-'.repeat(nameWidth), '-'.repeat(valWidth)], columnWidths), - ...rows.map((r) => formatAsciiRow([r.name, r.value], columnWidths)), - ] - return lines.join('\n') + const title = "Role Enforcements"; + if (rows.length === 0) return `${title}\n (none)`; + + const nameWidth = Math.max("Name".length, ...rows.map((r) => r.name.length)); + const valWidth = Math.max("Value".length, ...rows.map((r) => r.value.length)); + const columnWidths = [nameWidth, valWidth]; + + const lines: string[] = [ + title, + formatAsciiRow(["Name", "Value"], columnWidths), + formatAsciiRow(["-".repeat(nameWidth), "-".repeat(valWidth)], columnWidths), + ...rows.map((r) => formatAsciiRow([r.name, r.value], columnWidths)), + ]; + return lines.join("\n"); } -function renderManagedNodesSection(table: FormattedManagedNodePrivilegeTable): string { - const title = 'Managed Node Privileges' - const colHeaders = ['Privilege', ...table.nodeHeaders] - - const colWidths = colHeaders.map((h) => h.length) - for (const row of table.privilegeRows) { - colWidths[0] = Math.max(colWidths[0], row.privilege.length) - row.granted.forEach((_, i) => { - colWidths[i + 1] = Math.max(colWidths[i + 1], 1) - }) - } - colWidths[0] = Math.max(colWidths[0], 'Cascade Node Permissions'.length) - - const formatRow = (cells: string[]) => formatAsciiRow(cells, colWidths) - - const rule = formatRow(colWidths.map((w) => '-'.repeat(w))) - const lines: string[] = [ - title, - formatRow(colHeaders), - rule, - ...table.privilegeRows.map((row) => - formatRow([row.privilege, ...row.granted.map((g) => (g ? 'X' : ''))]) - ), - rule, - formatRow(['Cascade Node Permissions', ...table.cascadeRow.map((c) => (c ? 'X' : ''))]), - ] - return lines.join('\n') +function renderManagedNodesSection( + table: FormattedManagedNodePrivilegeTable, +): string { + const title = "Managed Node Privileges"; + const colHeaders = ["Privilege", ...table.nodeHeaders]; + + const colWidths = colHeaders.map((h) => h.length); + for (const row of table.privilegeRows) { + colWidths[0] = Math.max(colWidths[0], row.privilege.length); + row.granted.forEach((_, i) => { + colWidths[i + 1] = Math.max(colWidths[i + 1], 1); + }); + } + colWidths[0] = Math.max(colWidths[0], "Cascade Node Permissions".length); + + const formatRow = (cells: string[]) => formatAsciiRow(cells, colWidths); + + const rule = formatRow(colWidths.map((w) => "-".repeat(w))); + const lines: string[] = [ + title, + formatRow(colHeaders), + rule, + ...table.privilegeRows.map((row) => + formatRow([row.privilege, ...row.granted.map((g) => (g ? "X" : ""))]), + ), + rule, + formatRow([ + "Cascade Node Permissions", + ...table.cascadeRow.map((c) => (c ? "X" : "")), + ]), + ]; + return lines.join("\n"); } function boolText(value: boolean): string { - return value ? 'True' : 'False' + return value ? "True" : "False"; } function asLines(value: string | string[]): string[] { - if (Array.isArray(value)) return value.length > 0 ? value : [''] - return [value] + if (Array.isArray(value)) return value.length > 0 ? value : [""]; + return [value]; } function asIdLines(id: number | number[] | undefined): string[] { - if (id == null) return [] - if (Array.isArray(id)) return id.length > 0 ? id.map(String) : [''] - return [String(id)] + if (id == null) return []; + if (Array.isArray(id)) return id.length > 0 ? id.map(String) : [""]; + return [String(id)]; } function formatAsciiRow(cells: string[], widths: number[]): string { - return cells.map((cell, index) => cell + ' '.repeat(Math.max(0, widths[index] - cell.length))).join(' ') -} \ No newline at end of file + return cells + .map( + (cell, index) => + cell + " ".repeat(Math.max(0, widths[index] - cell.length)), + ) + .join(" "); +} diff --git a/KeeperSdk/src/sharedFolders/SharedFolderManager.ts b/KeeperSdk/src/sharedFolders/SharedFolderManager.ts index bac940f3..f088145d 100644 --- a/KeeperSdk/src/sharedFolders/SharedFolderManager.ts +++ b/KeeperSdk/src/sharedFolders/SharedFolderManager.ts @@ -1,57 +1,64 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError, ResultCodes } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { KeeperSdkError, ResultCodes } from "../utils"; import { - formatSharedFoldersTable, - listSharedFolders, - renderSharedFoldersAsciiTable, -} from './listSharedFolders' + formatSharedFoldersTable, + listSharedFolders, + renderSharedFoldersAsciiTable, +} from "./listSharedFolders"; import type { - FormattedSharedFoldersTable, - ListSharedFolderRow, - ListSharedFoldersOptions, -} from './listSharedFolders' -import { shareFolder } from './shareFolder' -import type { ShareFolderInput, ShareFolderResult } from './shareFolder' + FormattedSharedFoldersTable, + ListSharedFolderRow, + ListSharedFoldersOptions, +} from "./listSharedFolders"; +import { shareFolder } from "./shareFolder"; +import type { ShareFolderInput, ShareFolderResult } from "./shareFolder"; -export type AuthProvider = () => Auth +export type AuthProvider = () => Auth; export class SharedFolderManager { - private readonly storage: InMemoryStorage - private readonly authProvider: AuthProvider + private readonly storage: InMemoryStorage; + private readonly authProvider: AuthProvider; - constructor(storage: InMemoryStorage, authProvider: AuthProvider) { - this.storage = storage - this.authProvider = authProvider - } + constructor(storage: InMemoryStorage, authProvider: AuthProvider) { + this.storage = storage; + this.authProvider = authProvider; + } - private requireAuth(): Auth { - const auth = this.authProvider() - if (!auth) { - throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN) - } - return auth + private requireAuth(): Auth { + const auth = this.authProvider(); + if (!auth) { + throw new KeeperSdkError( + "Not logged in. Call login() first.", + ResultCodes.NOT_LOGGED_IN, + ); } + return auth; + } - public listSharedFolders(options: ListSharedFoldersOptions = {}): ListSharedFolderRow[] { - return listSharedFolders(this.storage, options) - } + public listSharedFolders( + options: ListSharedFoldersOptions = {}, + ): ListSharedFolderRow[] { + return listSharedFolders(this.storage, options); + } - public formatSharedFoldersTable( - rows: ListSharedFolderRow[], - options: { verbose?: boolean; columnWidth?: number } = {} - ): FormattedSharedFoldersTable { - return formatSharedFoldersTable(rows, options) - } + public formatSharedFoldersTable( + rows: ListSharedFolderRow[], + options: { verbose?: boolean; columnWidth?: number } = {}, + ): FormattedSharedFoldersTable { + return formatSharedFoldersTable(rows, options); + } - public renderSharedFoldersAsciiTable( - table: FormattedSharedFoldersTable, - options: { minColWidth?: number } = {} - ): string { - return renderSharedFoldersAsciiTable(table, options) - } + public renderSharedFoldersAsciiTable( + table: FormattedSharedFoldersTable, + options: { minColWidth?: number } = {}, + ): string { + return renderSharedFoldersAsciiTable(table, options); + } - public async shareFolder(input: ShareFolderInput): Promise { - return shareFolder(this.requireAuth(), this.storage, input) - } + public async shareFolder( + input: ShareFolderInput, + ): Promise { + return shareFolder(this.requireAuth(), this.storage, input); + } } diff --git a/KeeperSdk/src/sharedFolders/listSharedFolders.ts b/KeeperSdk/src/sharedFolders/listSharedFolders.ts index ac484bf3..660dc89a 100644 --- a/KeeperSdk/src/sharedFolders/listSharedFolders.ts +++ b/KeeperSdk/src/sharedFolders/listSharedFolders.ts @@ -1,173 +1,212 @@ import type { - DSharedFolder, - DSharedFolderRecord, - DSharedFolderTeam, - DSharedFolderUser, -} from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { TOKEN_SEPARATOR_PATTERN } from '../utils' -import { FolderKind, VaultObjectKind } from '../folders/folderHelpers' + DSharedFolder, + DSharedFolderRecord, + DSharedFolderTeam, + DSharedFolderUser, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { TOKEN_SEPARATOR_PATTERN } from "../utils"; +import { FolderKind, VaultObjectKind } from "../folders/folderHelpers"; export type ListSharedFoldersOptions = { - pattern?: string | null - verbose?: boolean - includeDetails?: boolean -} + pattern?: string | null; + verbose?: boolean; + includeDetails?: boolean; +}; export type ListSharedFolderRow = { - shared_folder_uid: string - name: string - team_count?: number - user_count?: number - record_count?: number - default_manage_records?: boolean - default_manage_users?: boolean - default_can_edit?: boolean - default_can_share?: boolean -} - -const DEFAULT_COLUMN_WIDTH = 40 -const MIN_TRUNCATE_PREFIX = 3 + shared_folder_uid: string; + name: string; + team_count?: number; + user_count?: number; + record_count?: number; + default_manage_records?: boolean; + default_manage_users?: boolean; + default_can_edit?: boolean; + default_can_share?: boolean; +}; + +const DEFAULT_COLUMN_WIDTH = 40; +const MIN_TRUNCATE_PREFIX = 3; function sharedFolderDisplayName(folder: DSharedFolder): string { - return (folder.name || folder.uid).trim() || folder.uid + return (folder.name || folder.uid).trim() || folder.uid; } -function findSharedFolders(storage: InMemoryStorage, pattern: string): DSharedFolder[] { - const searchWords = tokenize(pattern.toLowerCase()) - const matches: DSharedFolder[] = [] - for (const sharedFolder of storage.getAll(FolderKind.SharedFolder)) { - const uid = sharedFolder.uid - const name = sharedFolderDisplayName(sharedFolder).toLowerCase() - const entityWords = tokenize(`${uid} ${name}`) - if (matchEntity(entityWords, searchWords)) { - matches.push(sharedFolder) - } +function findSharedFolders( + storage: InMemoryStorage, + pattern: string, +): DSharedFolder[] { + const searchWords = tokenize(pattern.toLowerCase()); + const matches: DSharedFolder[] = []; + for (const sharedFolder of storage.getAll( + FolderKind.SharedFolder, + )) { + const uid = sharedFolder.uid; + const name = sharedFolderDisplayName(sharedFolder).toLowerCase(); + const entityWords = tokenize(`${uid} ${name}`); + if (matchEntity(entityWords, searchWords)) { + matches.push(sharedFolder); } - return matches + } + return matches; } function matchEntity(entityWords: string[], searchWords: string[]): boolean { - if (!searchWords || searchWords.length === 0) return true - if (!entityWords || entityWords.length === 0) return false - for (const entityWord of entityWords) { - for (const searchWord of searchWords) { - if (searchWord.length <= entityWord.length && entityWord.includes(searchWord)) { - return true - } - } + if (!searchWords || searchWords.length === 0) return true; + if (!entityWords || entityWords.length === 0) return false; + for (const entityWord of entityWords) { + for (const searchWord of searchWords) { + if ( + searchWord.length <= entityWord.length && + entityWord.includes(searchWord) + ) { + return true; + } } - return false + } + return false; } function tokenize(text: string): string[] { - return text.split(TOKEN_SEPARATOR_PATTERN).filter((token) => token.length > 0) + return text + .split(TOKEN_SEPARATOR_PATTERN) + .filter((token) => token.length > 0); } -function countBySharedFolderUid(items: T[], sharedFolderUid: string): number { - let count = 0 - for (const item of items) { - if (item.sharedFolderUid === sharedFolderUid) count += 1 - } - return count +function countBySharedFolderUid( + items: T[], + sharedFolderUid: string, +): number { + let count = 0; + for (const item of items) { + if (item.sharedFolderUid === sharedFolderUid) count += 1; + } + return count; } -function countTeamsForFolder(storage: InMemoryStorage, sharedFolderUid: string): number { - return countBySharedFolderUid(storage.getAll(VaultObjectKind.SharedFolderTeam), sharedFolderUid) +function countTeamsForFolder( + storage: InMemoryStorage, + sharedFolderUid: string, +): number { + return countBySharedFolderUid( + storage.getAll(VaultObjectKind.SharedFolderTeam), + sharedFolderUid, + ); } -function countUsersForFolder(storage: InMemoryStorage, sharedFolderUid: string): number { - return countBySharedFolderUid(storage.getAll(VaultObjectKind.SharedFolderUser), sharedFolderUid) +function countUsersForFolder( + storage: InMemoryStorage, + sharedFolderUid: string, +): number { + return countBySharedFolderUid( + storage.getAll(VaultObjectKind.SharedFolderUser), + sharedFolderUid, + ); } -function countRecordsForFolder(storage: InMemoryStorage, sharedFolderUid: string): number { - return countBySharedFolderUid( - storage.getAll(VaultObjectKind.SharedFolderRecord), - sharedFolderUid - ) +function countRecordsForFolder( + storage: InMemoryStorage, + sharedFolderUid: string, +): number { + return countBySharedFolderUid( + storage.getAll(VaultObjectKind.SharedFolderRecord), + sharedFolderUid, + ); } export function listSharedFolders( - storage: InMemoryStorage, - options: ListSharedFoldersOptions = {} + storage: InMemoryStorage, + options: ListSharedFoldersOptions = {}, ): ListSharedFolderRow[] { - const { pattern, includeDetails = false } = options - const sharedFolders: DSharedFolder[] = pattern - ? findSharedFolders(storage, pattern) - : storage.getAll(FolderKind.SharedFolder) - - return sharedFolders.map((sharedFolder) => { - const shared_folder_uid = sharedFolder.uid - const name = sharedFolderDisplayName(sharedFolder) - const row: ListSharedFolderRow = { shared_folder_uid, name } - if (includeDetails) { - row.record_count = countRecordsForFolder(storage, shared_folder_uid) - row.user_count = countUsersForFolder(storage, shared_folder_uid) - row.team_count = countTeamsForFolder(storage, shared_folder_uid) - row.default_manage_records = sharedFolder.defaultManageRecords - row.default_manage_users = sharedFolder.defaultManageUsers - row.default_can_edit = sharedFolder.defaultCanEdit - row.default_can_share = sharedFolder.defaultCanShare - } - return row - }) + const { pattern, includeDetails = false } = options; + const sharedFolders: DSharedFolder[] = pattern + ? findSharedFolders(storage, pattern) + : storage.getAll(FolderKind.SharedFolder); + + return sharedFolders.map((sharedFolder) => { + const shared_folder_uid = sharedFolder.uid; + const name = sharedFolderDisplayName(sharedFolder); + const row: ListSharedFolderRow = { shared_folder_uid, name }; + if (includeDetails) { + row.record_count = countRecordsForFolder(storage, shared_folder_uid); + row.user_count = countUsersForFolder(storage, shared_folder_uid); + row.team_count = countTeamsForFolder(storage, shared_folder_uid); + row.default_manage_records = sharedFolder.defaultManageRecords; + row.default_manage_users = sharedFolder.defaultManageUsers; + row.default_can_edit = sharedFolder.defaultCanEdit; + row.default_can_share = sharedFolder.defaultCanShare; + } + return row; + }); } export type FormattedSharedFoldersTable = { - headers: string[] - rows: string[][] -} + headers: string[]; + rows: string[][]; +}; function truncateText(text: string, maxLength: number): string { - if (!text) return '' - if (text.length <= maxLength) return text - if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength) - return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...` + if (!text) return ""; + if (text.length <= maxLength) return text; + if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength); + return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...`; } export function formatSharedFoldersTable( - rows: ListSharedFolderRow[], - options: { verbose?: boolean; columnWidth?: number } = {} + rows: ListSharedFolderRow[], + options: { verbose?: boolean; columnWidth?: number } = {}, ): FormattedSharedFoldersTable { - const { verbose = false, columnWidth = DEFAULT_COLUMN_WIDTH } = options - const maxWidth = verbose ? null : columnWidth - const headers = ['#', 'Shared Folder UID', 'Name'] - const outRows: string[][] = rows.map((row, rowIndex) => { - const uid = maxWidth == null ? row.shared_folder_uid : truncateText(row.shared_folder_uid, maxWidth) - const name = maxWidth == null ? row.name : truncateText(row.name, maxWidth) - return [String(rowIndex + 1), uid, name] - }) - return { headers, rows: outRows } + const { verbose = false, columnWidth = DEFAULT_COLUMN_WIDTH } = options; + const maxWidth = verbose ? null : columnWidth; + const headers = ["#", "Shared Folder UID", "Name"]; + const outRows: string[][] = rows.map((row, rowIndex) => { + const uid = + maxWidth == null + ? row.shared_folder_uid + : truncateText(row.shared_folder_uid, maxWidth); + const name = maxWidth == null ? row.name : truncateText(row.name, maxWidth); + return [String(rowIndex + 1), uid, name]; + }); + return { headers, rows: outRows }; } export function renderSharedFoldersAsciiTable( - table: FormattedSharedFoldersTable, - options: { minColWidth?: number } = {} + table: FormattedSharedFoldersTable, + options: { minColWidth?: number } = {}, ): string { - const { minColWidth = 2 } = options - const { headers, rows } = table - const columnCount = headers.length - const columnWidths: number[] = new Array(columnCount).fill(0) + const { minColWidth = 2 } = options; + const { headers, rows } = table; + const columnCount = headers.length; + const columnWidths: number[] = new Array(columnCount).fill(0); + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { + columnWidths[columnIndex] = Math.max( + headers[columnIndex].length, + minColWidth, + ); + } + for (const row of rows) { for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { - columnWidths[columnIndex] = Math.max(headers[columnIndex].length, minColWidth) - } - for (const row of rows) { - for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { - const cell = row[columnIndex] || '' - columnWidths[columnIndex] = Math.max(columnWidths[columnIndex], cell.length, minColWidth) - } - } - const padCell = (cell: string, columnIndex: number) => - cell + ' '.repeat(columnWidths[columnIndex] - cell.length) - const formatRow = (cells: string[]) => cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - const ruleRow = Array.from({ length: columnCount }, (_unused, columnIndex) => - '-'.repeat(columnWidths[columnIndex]) - ) - .map((dashes, columnIndex) => padCell(dashes, columnIndex)) - .join(' ') - const lines: string[] = [formatRow(headers), ruleRow] - for (const row of rows) { - lines.push(formatRow(row)) + const cell = row[columnIndex] || ""; + columnWidths[columnIndex] = Math.max( + columnWidths[columnIndex], + cell.length, + minColWidth, + ); } - return lines.join('\n') + } + const padCell = (cell: string, columnIndex: number) => + cell + " ".repeat(columnWidths[columnIndex] - cell.length); + const formatRow = (cells: string[]) => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + const ruleRow = Array.from({ length: columnCount }, (_unused, columnIndex) => + "-".repeat(columnWidths[columnIndex]), + ) + .map((dashes, columnIndex) => padCell(dashes, columnIndex)) + .join(" "); + const lines: string[] = [formatRow(headers), ruleRow]; + for (const row of rows) { + lines.push(formatRow(row)); + } + return lines.join("\n"); } diff --git a/KeeperSdk/src/sharedFolders/shareFolder.ts b/KeeperSdk/src/sharedFolders/shareFolder.ts index 98d622c9..d229390b 100644 --- a/KeeperSdk/src/sharedFolders/shareFolder.ts +++ b/KeeperSdk/src/sharedFolders/shareFolder.ts @@ -1,457 +1,546 @@ import type { - Auth, - Authentication, - DSharedFolder, - DSharedFolderFolder, - DSharedFolderUser, - DUserFolder, -} from '@keeper-security/keeperapi' + Auth, + Authentication, + DSharedFolder, + DSharedFolderFolder, + DSharedFolderUser, + DUserFolder, +} from "@keeper-security/keeperapi"; import { - Folder, - getPublicKeysMessage, - normal64Bytes, - platform, - sharedFolderUpdateV3Message, -} from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { extractErrorMessage, isBoolean, isObject, isValidEmail, KeeperSdkError } from '../utils' -import { FolderKind, FolderResultStatus, VaultObjectKind } from '../folders/folderHelpers' + Folder, + getPublicKeysMessage, + normal64Bytes, + platform, + sharedFolderUpdateV3Message, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { + extractErrorMessage, + isBoolean, + isObject, + isValidEmail, + KeeperSdkError, +} from "../utils"; +import { + FolderKind, + FolderResultStatus, + VaultObjectKind, +} from "../folders/folderHelpers"; export enum ShareFolderAction { - Grant = 'grant', - Remove = 'remove', + Grant = "grant", + Remove = "remove", } -export type ShareFolderActionInput = ShareFolderAction | `${ShareFolderAction}` +export type ShareFolderActionInput = ShareFolderAction | `${ShareFolderAction}`; export enum ShareFolderUserResultStatus { - Success = 'success', - Invited = 'invited', - MissingPublicKey = 'missing_public_key', - Unknown = 'unknown', + Success = "success", + Invited = "invited", + MissingPublicKey = "missing_public_key", + Unknown = "unknown", } export type ShareFolderInput = { - folder: string - emails: string[] - action?: ShareFolderActionInput - manageRecords?: boolean - manageUsers?: boolean -} + folder: string; + emails: string[]; + action?: ShareFolderActionInput; + manageRecords?: boolean; + manageUsers?: boolean; +}; export type ShareFolderUserStatus = { - email: string - success: boolean - status: string - message?: string -} + email: string; + success: boolean; + status: string; + message?: string; +}; export type ShareFolderResult = { - success: boolean - message?: string - folderUid: string - folderKind: FolderKind.SharedFolder - sharedFolderUid: string - results: ShareFolderUserStatus[] -} + success: boolean; + message?: string; + folderUid: string; + folderKind: FolderKind.SharedFolder; + sharedFolderUid: string; + results: ShareFolderUserStatus[]; +}; type ResolvedFolder = - | { - kind: FolderKind.SharedFolder - folderUid: string - sharedFolderUid: string - displayName: string - } - | { - kind: FolderKind.SharedFolderFolder - folderUid: string - sharedFolderUid: string - displayName: string - } - | { kind: FolderKind.UserFolder; folderUid: string; displayName: string } + | { + kind: FolderKind.SharedFolder; + folderUid: string; + sharedFolderUid: string; + displayName: string; + } + | { + kind: FolderKind.SharedFolderFolder; + folderUid: string; + sharedFolderUid: string; + displayName: string; + } + | { kind: FolderKind.UserFolder; folderUid: string; displayName: string }; type UserPublicKeys = { - rsaPublicKey: Uint8Array | null - eccPublicKey: Uint8Array | null - errorCode?: string - message?: string - username: string -} + rsaPublicKey: Uint8Array | null; + eccPublicKey: Uint8Array | null; + errorCode?: string; + message?: string; + username: string; +}; function toSetBoolean(value: boolean | undefined): Folder.SetBooleanValue { - if (value === true) return Folder.SetBooleanValue.BOOLEAN_TRUE - if (value === false) return Folder.SetBooleanValue.BOOLEAN_FALSE - return Folder.SetBooleanValue.BOOLEAN_NO_CHANGE + if (value === true) return Folder.SetBooleanValue.BOOLEAN_TRUE; + if (value === false) return Folder.SetBooleanValue.BOOLEAN_FALSE; + return Folder.SetBooleanValue.BOOLEAN_NO_CHANGE; } function dataName(data: unknown): string { - if (isObject(data)) { - const { title, name } = data as { title?: string; name?: string } - return (title || name || '').trim() - } - return '' + if (isObject(data)) { + const { title, name } = data as { title?: string; name?: string }; + return (title || name || "").trim(); + } + return ""; } -function resolveFolder(storage: InMemoryStorage, nameOrUid: string): ResolvedFolder | undefined { - const trimmedNameOrUid = nameOrUid.trim() - if (!trimmedNameOrUid) return undefined - - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, trimmedNameOrUid) - if (sharedFolder) { - return { - kind: FolderKind.SharedFolder, - folderUid: sharedFolder.uid, - sharedFolderUid: sharedFolder.uid, - displayName: (sharedFolder.name || sharedFolder.uid).trim() || sharedFolder.uid, - } - } - const sharedFolderFolder = storage.getByUid(FolderKind.SharedFolderFolder, trimmedNameOrUid) - if (sharedFolderFolder) { - return { - kind: FolderKind.SharedFolderFolder, - folderUid: sharedFolderFolder.uid, - sharedFolderUid: sharedFolderFolder.sharedFolderUid, - displayName: dataName(sharedFolderFolder.data) || sharedFolderFolder.uid, - } - } - const userFolder = storage.getByUid(FolderKind.UserFolder, trimmedNameOrUid) - if (userFolder) { - return { - kind: FolderKind.UserFolder, - folderUid: userFolder.uid, - displayName: dataName(userFolder.data) || userFolder.uid, - } - } - - const lowerNameOrUid = trimmedNameOrUid.toLowerCase() - for (const candidateSharedFolder of storage.getAll(FolderKind.SharedFolder)) { - if ((candidateSharedFolder.name || '').trim().toLowerCase() === lowerNameOrUid) { - return { - kind: FolderKind.SharedFolder, - folderUid: candidateSharedFolder.uid, - sharedFolderUid: candidateSharedFolder.uid, - displayName: candidateSharedFolder.name || candidateSharedFolder.uid, - } - } +function resolveFolder( + storage: InMemoryStorage, + nameOrUid: string, +): ResolvedFolder | undefined { + const trimmedNameOrUid = nameOrUid.trim(); + if (!trimmedNameOrUid) return undefined; + + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + trimmedNameOrUid, + ); + if (sharedFolder) { + return { + kind: FolderKind.SharedFolder, + folderUid: sharedFolder.uid, + sharedFolderUid: sharedFolder.uid, + displayName: + (sharedFolder.name || sharedFolder.uid).trim() || sharedFolder.uid, + }; + } + const sharedFolderFolder = storage.getByUid( + FolderKind.SharedFolderFolder, + trimmedNameOrUid, + ); + if (sharedFolderFolder) { + return { + kind: FolderKind.SharedFolderFolder, + folderUid: sharedFolderFolder.uid, + sharedFolderUid: sharedFolderFolder.sharedFolderUid, + displayName: dataName(sharedFolderFolder.data) || sharedFolderFolder.uid, + }; + } + const userFolder = storage.getByUid( + FolderKind.UserFolder, + trimmedNameOrUid, + ); + if (userFolder) { + return { + kind: FolderKind.UserFolder, + folderUid: userFolder.uid, + displayName: dataName(userFolder.data) || userFolder.uid, + }; + } + + const lowerNameOrUid = trimmedNameOrUid.toLowerCase(); + for (const candidateSharedFolder of storage.getAll( + FolderKind.SharedFolder, + )) { + if ( + (candidateSharedFolder.name || "").trim().toLowerCase() === lowerNameOrUid + ) { + return { + kind: FolderKind.SharedFolder, + folderUid: candidateSharedFolder.uid, + sharedFolderUid: candidateSharedFolder.uid, + displayName: candidateSharedFolder.name || candidateSharedFolder.uid, + }; } - for (const candidateSharedFolderFolder of storage.getAll(FolderKind.SharedFolderFolder)) { - const candidateName = dataName(candidateSharedFolderFolder.data) - if (candidateName && candidateName.toLowerCase() === lowerNameOrUid) { - return { - kind: FolderKind.SharedFolderFolder, - folderUid: candidateSharedFolderFolder.uid, - sharedFolderUid: candidateSharedFolderFolder.sharedFolderUid, - displayName: candidateName, - } - } + } + for (const candidateSharedFolderFolder of storage.getAll( + FolderKind.SharedFolderFolder, + )) { + const candidateName = dataName(candidateSharedFolderFolder.data); + if (candidateName && candidateName.toLowerCase() === lowerNameOrUid) { + return { + kind: FolderKind.SharedFolderFolder, + folderUid: candidateSharedFolderFolder.uid, + sharedFolderUid: candidateSharedFolderFolder.sharedFolderUid, + displayName: candidateName, + }; } - for (const candidateUserFolder of storage.getAll(FolderKind.UserFolder)) { - const candidateName = dataName(candidateUserFolder.data) - if (candidateName && candidateName.toLowerCase() === lowerNameOrUid) { - return { - kind: FolderKind.UserFolder, - folderUid: candidateUserFolder.uid, - displayName: candidateName, - } - } + } + for (const candidateUserFolder of storage.getAll( + FolderKind.UserFolder, + )) { + const candidateName = dataName(candidateUserFolder.data); + if (candidateName && candidateName.toLowerCase() === lowerNameOrUid) { + return { + kind: FolderKind.UserFolder, + folderUid: candidateUserFolder.uid, + displayName: candidateName, + }; } + } - return undefined + return undefined; } -async function fetchUserPublicKeys(auth: Auth, emails: string[]): Promise> { - const usernameToKeys = new Map() - if (emails.length === 0) return usernameToKeys - - const keysRequest = getPublicKeysMessage({ usernames: emails }) - let response: Authentication.IGetPublicKeysResponse - try { - response = await auth.executeRest(keysRequest) - } catch (err) { - throw new KeeperSdkError(`Failed to fetch public keys: ${extractErrorMessage(err)}`) - } - for (const entry of response.keyResponses || []) { - const username = (entry.username || '').toLowerCase() - if (!username) continue - usernameToKeys.set(username, { - username: entry.username || '', - rsaPublicKey: entry.publicKey && entry.publicKey.length > 0 ? (entry.publicKey as Uint8Array) : null, - eccPublicKey: - entry.publicEccKey && entry.publicEccKey.length > 0 ? (entry.publicEccKey as Uint8Array) : null, - errorCode: entry.errorCode || undefined, - message: entry.message || undefined, - }) - } - return usernameToKeys +async function fetchUserPublicKeys( + auth: Auth, + emails: string[], +): Promise> { + const usernameToKeys = new Map(); + if (emails.length === 0) return usernameToKeys; + + const keysRequest = getPublicKeysMessage({ usernames: emails }); + let response: Authentication.IGetPublicKeysResponse; + try { + response = await auth.executeRest(keysRequest); + } catch (err) { + throw new KeeperSdkError( + `Failed to fetch public keys: ${extractErrorMessage(err)}`, + ); + } + for (const entry of response.keyResponses || []) { + const username = (entry.username || "").toLowerCase(); + if (!username) continue; + usernameToKeys.set(username, { + username: entry.username || "", + rsaPublicKey: + entry.publicKey && entry.publicKey.length > 0 + ? (entry.publicKey as Uint8Array) + : null, + eccPublicKey: + entry.publicEccKey && entry.publicEccKey.length > 0 + ? (entry.publicEccKey as Uint8Array) + : null, + errorCode: entry.errorCode || undefined, + message: entry.message || undefined, + }); + } + return usernameToKeys; } function dedupeEmails(emails: string[]): string[] { - const seen = new Set() - const dedupedEmails: string[] = [] - for (const rawEmail of emails) { - const normalized = (rawEmail || '').trim().toLowerCase() - if (!normalized) continue - if (seen.has(normalized)) continue - seen.add(normalized) - dedupedEmails.push(normalized) - } - return dedupedEmails + const seen = new Set(); + const dedupedEmails: string[] = []; + for (const rawEmail of emails) { + const normalized = (rawEmail || "").trim().toLowerCase(); + if (!normalized) continue; + if (seen.has(normalized)) continue; + seen.add(normalized); + dedupedEmails.push(normalized); + } + return dedupedEmails; } async function removeFromSharedFolder( - auth: Auth, - sharedFolder: DSharedFolder, - resolved: Extract, - emails: string[] + auth: Auth, + sharedFolder: DSharedFolder, + resolved: Extract< + ResolvedFolder, + { kind: FolderKind.SharedFolder | FolderKind.SharedFolderFolder } + >, + emails: string[], ): Promise { - const updateRequest: Folder.ISharedFolderUpdateV3Request = { - sharedFolderUid: normal64Bytes(sharedFolder.uid), - revision: sharedFolder.revision, - forceUpdate: false, - sharedFolderRemoveUser: emails, - } - - let response: Folder.ISharedFolderUpdateV3ResponseV2 - try { - response = await auth.executeRest(sharedFolderUpdateV3Message({ sharedFoldersUpdateV3: [updateRequest] })) - } catch (err) { - return { - success: false, - folderUid: resolved.folderUid, - sharedFolderUid: sharedFolder.uid, - folderKind: FolderKind.SharedFolder, - message: `shared_folder_update_v3 (remove) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): ${extractErrorMessage(err)}`, - results: [], - } - } - - const innerResponse = (response.sharedFoldersUpdateV3Response || [])[0] - const requestOk = !innerResponse?.status || innerResponse.status === FolderResultStatus.Success - - const userResults: ShareFolderUserStatus[] = [] - for (const userStatus of innerResponse?.sharedFolderRemoveUserStatus || []) { - const status = userStatus.status || ShareFolderUserResultStatus.Unknown - userResults.push({ - email: userStatus.username || '', - success: status === FolderResultStatus.Success, - status, - }) - } - - const allUsersOk = userResults.length > 0 && userResults.every((userResult) => userResult.success) - - const failureReason = !requestOk - ? innerResponse?.status || - `shared_folder_update_v3 (remove) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): server returned no status` - : undefined - + const updateRequest: Folder.ISharedFolderUpdateV3Request = { + sharedFolderUid: normal64Bytes(sharedFolder.uid), + revision: sharedFolder.revision, + forceUpdate: false, + sharedFolderRemoveUser: emails, + }; + + let response: Folder.ISharedFolderUpdateV3ResponseV2; + try { + response = await auth.executeRest( + sharedFolderUpdateV3Message({ sharedFoldersUpdateV3: [updateRequest] }), + ); + } catch (err) { return { - success: requestOk && allUsersOk, - folderUid: resolved.folderUid, - sharedFolderUid: sharedFolder.uid, - folderKind: FolderKind.SharedFolder, - message: failureReason, - results: userResults, - } + success: false, + folderUid: resolved.folderUid, + sharedFolderUid: sharedFolder.uid, + folderKind: FolderKind.SharedFolder, + message: `shared_folder_update_v3 (remove) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): ${extractErrorMessage(err)}`, + results: [], + }; + } + + const innerResponse = (response.sharedFoldersUpdateV3Response || [])[0]; + const requestOk = + !innerResponse?.status || + innerResponse.status === FolderResultStatus.Success; + + const userResults: ShareFolderUserStatus[] = []; + for (const userStatus of innerResponse?.sharedFolderRemoveUserStatus || []) { + const status = userStatus.status || ShareFolderUserResultStatus.Unknown; + userResults.push({ + email: userStatus.username || "", + success: status === FolderResultStatus.Success, + status, + }); + } + + const allUsersOk = + userResults.length > 0 && + userResults.every((userResult) => userResult.success); + + const failureReason = !requestOk + ? innerResponse?.status || + `shared_folder_update_v3 (remove) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): server returned no status` + : undefined; + + return { + success: requestOk && allUsersOk, + folderUid: resolved.folderUid, + sharedFolderUid: sharedFolder.uid, + folderKind: FolderKind.SharedFolder, + message: failureReason, + results: userResults, + }; } async function shareWithSharedFolder( - auth: Auth, - storage: InMemoryStorage, - resolved: Extract, - input: ShareFolderInput + auth: Auth, + storage: InMemoryStorage, + resolved: Extract< + ResolvedFolder, + { kind: FolderKind.SharedFolder | FolderKind.SharedFolderFolder } + >, + input: ShareFolderInput, ): Promise { - const sharedFolder = storage.getByUid(FolderKind.SharedFolder, resolved.sharedFolderUid) - if (!sharedFolder) { - throw new KeeperSdkError(`Shared folder "${resolved.sharedFolderUid}" not found.`, 'shared_folder_not_found') - } - const sharedFolderKey = await storage.getKeyBytes(sharedFolder.uid) - if (!sharedFolderKey) { - throw new KeeperSdkError( - 'Shared folder encryption key not available. Sync the vault and try again.', - 'shared_folder_key_missing' - ) - } - - const emails = dedupeEmails(input.emails) - if (emails.length === 0) { - throw new KeeperSdkError('Provide at least one user email.', 'no_emails') - } - - const invalidEmails = emails.filter((email) => !isValidEmail(email)) - if (invalidEmails.length > 0) { - throw new KeeperSdkError(`Invalid email(s): ${invalidEmails.join(', ')}`, 'invalid_email') - } - - if (input.action === ShareFolderAction.Remove) { - return removeFromSharedFolder(auth, sharedFolder, resolved, emails) - } - - const existingMembers = new Set() - for (const sharedFolderUser of storage.getAll(VaultObjectKind.SharedFolderUser)) { - if (sharedFolderUser.sharedFolderUid === sharedFolder.uid && sharedFolderUser.accountUsername) { - existingMembers.add(sharedFolderUser.accountUsername.toLowerCase()) - } - } - - const newEmails = emails.filter((email) => !existingMembers.has(email)) - const usernameToKeys = await fetchUserPublicKeys(auth, newEmails) - - const usersToAdd: Folder.ISharedFolderUpdateUser[] = [] - const usersToUpdate: Folder.ISharedFolderUpdateUser[] = [] - const userResults: ShareFolderUserStatus[] = [] - - const newUserManageRecords = isBoolean(input.manageRecords) - ? input.manageRecords - : sharedFolder.defaultManageRecords - const newUserManageUsers = isBoolean(input.manageUsers) - ? input.manageUsers - : sharedFolder.defaultManageUsers - - for (const email of emails) { - if (existingMembers.has(email)) { - usersToUpdate.push({ - username: email, - manageRecords: toSetBoolean(input.manageRecords), - manageUsers: toSetBoolean(input.manageUsers), - }) - continue - } - - const publicKeys = usernameToKeys.get(email) - if (!publicKeys) { - userResults.push({ - email, - success: false, - status: ShareFolderUserResultStatus.MissingPublicKey, - message: `No public key returned for user "${email}" (folder="${resolved.displayName}")`, - }) - continue - } - if (publicKeys.errorCode) { - userResults.push({ - email, - success: false, - status: publicKeys.errorCode, - message: publicKeys.message || publicKeys.errorCode, - }) - continue - } - - let encryptedKey: Uint8Array - let encryptedKeyType: Folder.EncryptedKeyType - if (publicKeys.rsaPublicKey) { - const rsaPublicKeyBase64 = platform.bytesToBase64(publicKeys.rsaPublicKey) - encryptedKey = platform.publicEncrypt(sharedFolderKey, rsaPublicKeyBase64) - encryptedKeyType = Folder.EncryptedKeyType.encrypted_by_public_key - } else if (publicKeys.eccPublicKey) { - encryptedKey = await platform.publicEncryptEC(sharedFolderKey, publicKeys.eccPublicKey) - encryptedKeyType = Folder.EncryptedKeyType.encrypted_by_public_key_ecc - } else { - userResults.push({ - email, - success: false, - status: ShareFolderUserResultStatus.MissingPublicKey, - message: `No usable public key for user "${email}" (folder="${resolved.displayName}")`, - }) - continue - } - - usersToAdd.push({ - username: email, - manageRecords: toSetBoolean(newUserManageRecords), - manageUsers: toSetBoolean(newUserManageUsers), - typedSharedFolderKey: { encryptedKey, encryptedKeyType }, - }) + const sharedFolder = storage.getByUid( + FolderKind.SharedFolder, + resolved.sharedFolderUid, + ); + if (!sharedFolder) { + throw new KeeperSdkError( + `Shared folder "${resolved.sharedFolderUid}" not found.`, + "shared_folder_not_found", + ); + } + const sharedFolderKey = await storage.getKeyBytes(sharedFolder.uid); + if (!sharedFolderKey) { + throw new KeeperSdkError( + "Shared folder encryption key not available. Sync the vault and try again.", + "shared_folder_key_missing", + ); + } + + const emails = dedupeEmails(input.emails); + if (emails.length === 0) { + throw new KeeperSdkError("Provide at least one user email.", "no_emails"); + } + + const invalidEmails = emails.filter((email) => !isValidEmail(email)); + if (invalidEmails.length > 0) { + throw new KeeperSdkError( + `Invalid email(s): ${invalidEmails.join(", ")}`, + "invalid_email", + ); + } + + if (input.action === ShareFolderAction.Remove) { + return removeFromSharedFolder(auth, sharedFolder, resolved, emails); + } + + const existingMembers = new Set(); + for (const sharedFolderUser of storage.getAll( + VaultObjectKind.SharedFolderUser, + )) { + if ( + sharedFolderUser.sharedFolderUid === sharedFolder.uid && + sharedFolderUser.accountUsername + ) { + existingMembers.add(sharedFolderUser.accountUsername.toLowerCase()); } - - if (usersToAdd.length === 0 && usersToUpdate.length === 0) { - return { - success: false, - folderUid: resolved.folderUid, - sharedFolderUid: sharedFolder.uid, - folderKind: FolderKind.SharedFolder, - message: `No users could be processed for shared folder "${resolved.displayName}" (uid=${sharedFolder.uid}).`, - results: userResults, - } + } + + const newEmails = emails.filter((email) => !existingMembers.has(email)); + const usernameToKeys = await fetchUserPublicKeys(auth, newEmails); + + const usersToAdd: Folder.ISharedFolderUpdateUser[] = []; + const usersToUpdate: Folder.ISharedFolderUpdateUser[] = []; + const userResults: ShareFolderUserStatus[] = []; + + const newUserManageRecords = isBoolean(input.manageRecords) + ? input.manageRecords + : sharedFolder.defaultManageRecords; + const newUserManageUsers = isBoolean(input.manageUsers) + ? input.manageUsers + : sharedFolder.defaultManageUsers; + + for (const email of emails) { + if (existingMembers.has(email)) { + usersToUpdate.push({ + username: email, + manageRecords: toSetBoolean(input.manageRecords), + manageUsers: toSetBoolean(input.manageUsers), + }); + continue; } - const updateRequest: Folder.ISharedFolderUpdateV3Request = { - sharedFolderUid: normal64Bytes(sharedFolder.uid), - revision: sharedFolder.revision, - forceUpdate: false, + const publicKeys = usernameToKeys.get(email); + if (!publicKeys) { + userResults.push({ + email, + success: false, + status: ShareFolderUserResultStatus.MissingPublicKey, + message: `No public key returned for user "${email}" (folder="${resolved.displayName}")`, + }); + continue; } - if (usersToAdd.length > 0) updateRequest.sharedFolderAddUser = usersToAdd - if (usersToUpdate.length > 0) updateRequest.sharedFolderUpdateUser = usersToUpdate - - let response: Folder.ISharedFolderUpdateV3ResponseV2 - try { - response = await auth.executeRest(sharedFolderUpdateV3Message({ sharedFoldersUpdateV3: [updateRequest] })) - } catch (err) { - return { - success: false, - folderUid: resolved.folderUid, - sharedFolderUid: sharedFolder.uid, - folderKind: FolderKind.SharedFolder, - message: `shared_folder_update_v3 (grant) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): ${extractErrorMessage(err)}`, - results: userResults, - } + if (publicKeys.errorCode) { + userResults.push({ + email, + success: false, + status: publicKeys.errorCode, + message: publicKeys.message || publicKeys.errorCode, + }); + continue; } - const innerResponse = (response.sharedFoldersUpdateV3Response || [])[0] - const requestOk = !innerResponse?.status || innerResponse.status === FolderResultStatus.Success - - for (const addUserStatus of innerResponse?.sharedFolderAddUserStatus || []) { - const status = addUserStatus.status || ShareFolderUserResultStatus.Unknown - const success = status === FolderResultStatus.Success || status === FolderResultStatus.Invited - userResults.push({ - email: addUserStatus.username || '', - success, - status, - }) - } - for (const updateUserStatus of innerResponse?.sharedFolderUpdateUserStatus || []) { - const status = updateUserStatus.status || ShareFolderUserResultStatus.Unknown - userResults.push({ - email: updateUserStatus.username || '', - success: status === FolderResultStatus.Success, - status, - }) + let encryptedKey: Uint8Array; + let encryptedKeyType: Folder.EncryptedKeyType; + if (publicKeys.rsaPublicKey) { + const rsaPublicKeyBase64 = platform.bytesToBase64( + publicKeys.rsaPublicKey, + ); + encryptedKey = platform.publicEncrypt( + sharedFolderKey, + rsaPublicKeyBase64, + ); + encryptedKeyType = Folder.EncryptedKeyType.encrypted_by_public_key; + } else if (publicKeys.eccPublicKey) { + encryptedKey = await platform.publicEncryptEC( + sharedFolderKey, + publicKeys.eccPublicKey, + ); + encryptedKeyType = Folder.EncryptedKeyType.encrypted_by_public_key_ecc; + } else { + userResults.push({ + email, + success: false, + status: ShareFolderUserResultStatus.MissingPublicKey, + message: `No usable public key for user "${email}" (folder="${resolved.displayName}")`, + }); + continue; } - const allUsersOk = userResults.length > 0 && userResults.every((userResult) => userResult.success) - - const failureReason = !requestOk - ? innerResponse?.status || - `shared_folder_update_v3 (grant) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): server returned no status` - : undefined + usersToAdd.push({ + username: email, + manageRecords: toSetBoolean(newUserManageRecords), + manageUsers: toSetBoolean(newUserManageUsers), + typedSharedFolderKey: { encryptedKey, encryptedKeyType }, + }); + } + if (usersToAdd.length === 0 && usersToUpdate.length === 0) { return { - success: requestOk && allUsersOk, - folderUid: resolved.folderUid, - sharedFolderUid: sharedFolder.uid, - folderKind: FolderKind.SharedFolder, - message: failureReason, - results: userResults, - } + success: false, + folderUid: resolved.folderUid, + sharedFolderUid: sharedFolder.uid, + folderKind: FolderKind.SharedFolder, + message: `No users could be processed for shared folder "${resolved.displayName}" (uid=${sharedFolder.uid}).`, + results: userResults, + }; + } + + const updateRequest: Folder.ISharedFolderUpdateV3Request = { + sharedFolderUid: normal64Bytes(sharedFolder.uid), + revision: sharedFolder.revision, + forceUpdate: false, + }; + if (usersToAdd.length > 0) updateRequest.sharedFolderAddUser = usersToAdd; + if (usersToUpdate.length > 0) + updateRequest.sharedFolderUpdateUser = usersToUpdate; + + let response: Folder.ISharedFolderUpdateV3ResponseV2; + try { + response = await auth.executeRest( + sharedFolderUpdateV3Message({ sharedFoldersUpdateV3: [updateRequest] }), + ); + } catch (err) { + return { + success: false, + folderUid: resolved.folderUid, + sharedFolderUid: sharedFolder.uid, + folderKind: FolderKind.SharedFolder, + message: `shared_folder_update_v3 (grant) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): ${extractErrorMessage(err)}`, + results: userResults, + }; + } + + const innerResponse = (response.sharedFoldersUpdateV3Response || [])[0]; + const requestOk = + !innerResponse?.status || + innerResponse.status === FolderResultStatus.Success; + + for (const addUserStatus of innerResponse?.sharedFolderAddUserStatus || []) { + const status = addUserStatus.status || ShareFolderUserResultStatus.Unknown; + const success = + status === FolderResultStatus.Success || + status === FolderResultStatus.Invited; + userResults.push({ + email: addUserStatus.username || "", + success, + status, + }); + } + for (const updateUserStatus of innerResponse?.sharedFolderUpdateUserStatus || + []) { + const status = + updateUserStatus.status || ShareFolderUserResultStatus.Unknown; + userResults.push({ + email: updateUserStatus.username || "", + success: status === FolderResultStatus.Success, + status, + }); + } + + const allUsersOk = + userResults.length > 0 && + userResults.every((userResult) => userResult.success); + + const failureReason = !requestOk + ? innerResponse?.status || + `shared_folder_update_v3 (grant) failed for "${resolved.displayName}" (uid=${sharedFolder.uid}): server returned no status` + : undefined; + + return { + success: requestOk && allUsersOk, + folderUid: resolved.folderUid, + sharedFolderUid: sharedFolder.uid, + folderKind: FolderKind.SharedFolder, + message: failureReason, + results: userResults, + }; } export async function shareFolder( - auth: Auth, - storage: InMemoryStorage, - input: ShareFolderInput + auth: Auth, + storage: InMemoryStorage, + input: ShareFolderInput, ): Promise { - const resolved = resolveFolder(storage, input.folder) - if (!resolved) { - throw new KeeperSdkError(`Folder "${input.folder}" was not found.`, 'folder_not_found') - } - - if (resolved.kind === FolderKind.UserFolder) { - throw new KeeperSdkError( - `"${resolved.displayName}" is a personal folder. Only shared folders can be shared.`, - 'not_a_shared_folder' - ) - } - - return shareWithSharedFolder(auth, storage, resolved, input) + const resolved = resolveFolder(storage, input.folder); + if (!resolved) { + throw new KeeperSdkError( + `Folder "${input.folder}" was not found.`, + "folder_not_found", + ); + } + + if (resolved.kind === FolderKind.UserFolder) { + throw new KeeperSdkError( + `"${resolved.displayName}" is a personal folder. Only shared folders can be shared.`, + "not_a_shared_folder", + ); + } + + return shareWithSharedFolder(auth, storage, resolved, input); } diff --git a/KeeperSdk/src/sharing/Sharing.ts b/KeeperSdk/src/sharing/Sharing.ts index 9046f369..6ca20049 100644 --- a/KeeperSdk/src/sharing/Sharing.ts +++ b/KeeperSdk/src/sharing/Sharing.ts @@ -1,282 +1,310 @@ import { - Auth, - Records, - Authentication, - platform, - getPublicKeysMessage, - getRecordsDetailsMessage, - webSafe64FromBytes, - recordsShareUpdateMessage, - normal64Bytes, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError } from '../utils/errors' + Auth, + Records, + Authentication, + platform, + getPublicKeysMessage, + getRecordsDetailsMessage, + webSafe64FromBytes, + recordsShareUpdateMessage, + normal64Bytes, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError } from "../utils/errors"; enum ShareStatus { - Success = 'success', - PendingAccept = 'pending_accept', - MissingPublicKey = 'missing_public_key', - Error = 'error', - Unknown = 'unknown', + Success = "success", + PendingAccept = "pending_accept", + MissingPublicKey = "missing_public_key", + Error = "error", + Unknown = "unknown", } export type ShareRecordInput = { - recordUid: string - email: string - canEdit?: boolean - canShare?: boolean -} + recordUid: string; + email: string; + canEdit?: boolean; + canShare?: boolean; +}; export type ShareRecordResult = { - recordUid: string - email: string - success: boolean - status: string - message: string -} + recordUid: string; + email: string; + success: boolean; + status: string; + message: string; +}; export type RemoveShareInput = { - recordUid: string - email: string -} + recordUid: string; + email: string; +}; export type RemoveShareResult = { - recordUid: string - email: string - success: boolean - status: string - message: string -} + recordUid: string; + email: string; + success: boolean; + status: string; + message: string; +}; type UserKeys = { - username: string - rsaPublicKey: Uint8Array | null - eccPublicKey: Uint8Array | null - errorCode: string | null -} + username: string; + rsaPublicKey: Uint8Array | null; + eccPublicKey: Uint8Array | null; + errorCode: string | null; +}; async function loadUserPublicKey(auth: Auth, email: string): Promise { - const msg = getPublicKeysMessage({ usernames: [email] }) - let response: Authentication.IGetPublicKeysResponse - - try { - response = await auth.executeRest(msg) - } catch (err) { - throw new KeeperSdkError(`Failed to fetch public key for ${email}: ${extractErrorMessage(err)}`) - } - - const keyResponses = response.keyResponses || [] - if (keyResponses.length === 0) { - throw new KeeperSdkError(`No public key returned for ${email}`, 'missing_public_key') - } - - const entry = keyResponses[0] - if (entry.errorCode) { - throw new KeeperSdkError( - `Public key lookup failed for ${email}: ${entry.errorCode} - ${entry.message || ''}`, - entry.errorCode - ) - } - - return { - username: entry.username || email, - rsaPublicKey: entry.publicKey && entry.publicKey.length > 0 ? (entry.publicKey as Uint8Array) : null, - eccPublicKey: entry.publicEccKey && entry.publicEccKey.length > 0 ? (entry.publicEccKey as Uint8Array) : null, - errorCode: entry.errorCode || null, - } + const msg = getPublicKeysMessage({ usernames: [email] }); + let response: Authentication.IGetPublicKeysResponse; + + try { + response = await auth.executeRest(msg); + } catch (err) { + throw new KeeperSdkError( + `Failed to fetch public key for ${email}: ${extractErrorMessage(err)}`, + ); + } + + const keyResponses = response.keyResponses || []; + if (keyResponses.length === 0) { + throw new KeeperSdkError( + `No public key returned for ${email}`, + "missing_public_key", + ); + } + + const entry = keyResponses[0]; + if (entry.errorCode) { + throw new KeeperSdkError( + `Public key lookup failed for ${email}: ${entry.errorCode} - ${entry.message || ""}`, + entry.errorCode, + ); + } + + return { + username: entry.username || email, + rsaPublicKey: + entry.publicKey && entry.publicKey.length > 0 + ? (entry.publicKey as Uint8Array) + : null, + eccPublicKey: + entry.publicEccKey && entry.publicEccKey.length > 0 + ? (entry.publicEccKey as Uint8Array) + : null, + errorCode: entry.errorCode || null, + }; } export async function shareRecord( - auth: Auth, - recordKey: Uint8Array, - input: ShareRecordInput + auth: Auth, + recordKey: Uint8Array, + input: ShareRecordInput, ): Promise { - const { recordUid, email, canEdit = false, canShare = false } = input - - const userKeys = await loadUserPublicKey(auth, email) - - let encryptedRecordKey: Uint8Array - let useEccKey = false - - if (userKeys.eccPublicKey) { - encryptedRecordKey = await platform.publicEncryptEC(recordKey, userKeys.eccPublicKey) - useEccKey = true - } else if (userKeys.rsaPublicKey) { - const rsaKeyBase64 = platform.bytesToBase64(userKeys.rsaPublicKey) - encryptedRecordKey = platform.publicEncrypt(recordKey, rsaKeyBase64) - useEccKey = false - } else { - return { - recordUid, - email, - success: false, - status: ShareStatus.MissingPublicKey, - message: `No usable public key available for ${email}`, - } - } - - const sharedRecord: Records.ISharedRecord = { - toUsername: email, - recordUid: normal64Bytes(recordUid), - recordKey: encryptedRecordKey, - editable: canEdit, - shareable: canShare, - useEccKey, - } - - const msg = recordsShareUpdateMessage({ addSharedRecord: [sharedRecord] }) - - let response: Records.IRecordShareUpdateResponse - try { - response = await auth.executeRest(msg) - } catch (err) { - return { - recordUid, - email, - success: false, - status: ShareStatus.Error, - message: extractErrorMessage(err), - } - } - - const addStatuses = response.addSharedRecordStatus || [] - if (addStatuses.length > 0) { - const st = addStatuses[0] - const isSuccess = st.status === ShareStatus.Success || st.status === ShareStatus.PendingAccept - return { - recordUid, - email: st.username || email, - success: isSuccess, - status: st.status || ShareStatus.Unknown, - message: st.message || st.status || '', - } - } - + const { recordUid, email, canEdit = false, canShare = false } = input; + + const userKeys = await loadUserPublicKey(auth, email); + + let encryptedRecordKey: Uint8Array; + let useEccKey = false; + + if (userKeys.eccPublicKey) { + encryptedRecordKey = await platform.publicEncryptEC( + recordKey, + userKeys.eccPublicKey, + ); + useEccKey = true; + } else if (userKeys.rsaPublicKey) { + const rsaKeyBase64 = platform.bytesToBase64(userKeys.rsaPublicKey); + encryptedRecordKey = platform.publicEncrypt(recordKey, rsaKeyBase64); + useEccKey = false; + } else { + return { + recordUid, + email, + success: false, + status: ShareStatus.MissingPublicKey, + message: `No usable public key available for ${email}`, + }; + } + + const sharedRecord: Records.ISharedRecord = { + toUsername: email, + recordUid: normal64Bytes(recordUid), + recordKey: encryptedRecordKey, + editable: canEdit, + shareable: canShare, + useEccKey, + }; + + const msg = recordsShareUpdateMessage({ addSharedRecord: [sharedRecord] }); + + let response: Records.IRecordShareUpdateResponse; + try { + response = await auth.executeRest(msg); + } catch (err) { + return { + recordUid, + email, + success: false, + status: ShareStatus.Error, + message: extractErrorMessage(err), + }; + } + + const addStatuses = response.addSharedRecordStatus || []; + if (addStatuses.length > 0) { + const st = addStatuses[0]; + const isSuccess = + st.status === ShareStatus.Success || + st.status === ShareStatus.PendingAccept; return { - recordUid, - email, - success: true, - status: ShareStatus.Success, - message: 'Record shared successfully', - } + recordUid, + email: st.username || email, + success: isSuccess, + status: st.status || ShareStatus.Unknown, + message: st.message || st.status || "", + }; + } + + return { + recordUid, + email, + success: true, + status: ShareStatus.Success, + message: "Record shared successfully", + }; } -export async function removeRecordShare(auth: Auth, input: RemoveShareInput): Promise { - const { recordUid, email } = input - - const msg = recordsShareUpdateMessage({ - removeSharedRecord: [ - { - toUsername: email, - recordUid: normal64Bytes(recordUid), - }, - ], - }) - - let response: Records.IRecordShareUpdateResponse - try { - response = await auth.executeRest(msg) - } catch (err) { - return { - recordUid, - email, - success: false, - status: ShareStatus.Error, - message: extractErrorMessage(err), - } - } - - const removeStatuses = response.removeSharedRecordStatus || [] - if (removeStatuses.length > 0) { - const st = removeStatuses[0] - return { - recordUid, - email: st.username || email, - success: st.status === ShareStatus.Success, - status: st.status || ShareStatus.Unknown, - message: st.message || st.status || '', - } - } +export async function removeRecordShare( + auth: Auth, + input: RemoveShareInput, +): Promise { + const { recordUid, email } = input; + const msg = recordsShareUpdateMessage({ + removeSharedRecord: [ + { + toUsername: email, + recordUid: normal64Bytes(recordUid), + }, + ], + }); + + let response: Records.IRecordShareUpdateResponse; + try { + response = await auth.executeRest(msg); + } catch (err) { return { - recordUid, - email, - success: true, - status: ShareStatus.Success, - message: 'Share removed successfully', - } + recordUid, + email, + success: false, + status: ShareStatus.Error, + message: extractErrorMessage(err), + }; + } + + const removeStatuses = response.removeSharedRecordStatus || []; + if (removeStatuses.length > 0) { + const st = removeStatuses[0]; + return { + recordUid, + email: st.username || email, + success: st.status === ShareStatus.Success, + status: st.status || ShareStatus.Unknown, + message: st.message || st.status || "", + }; + } + + return { + recordUid, + email, + success: true, + status: ShareStatus.Success, + message: "Share removed successfully", + }; } export type RecordUserPermission = { - username: string - accountUid?: string - owner: boolean - shareAdmin: boolean - shareable: boolean - editable: boolean - awaitingApproval: boolean - expiration?: number -} + username: string; + accountUid?: string; + owner: boolean; + shareAdmin: boolean; + shareable: boolean; + editable: boolean; + awaitingApproval: boolean; + expiration?: number; +}; export type RecordSharedFolderPermission = { - sharedFolderUid: string - resharable: boolean - editable: boolean - revision?: number - expiration?: number -} + sharedFolderUid: string; + resharable: boolean; + editable: boolean; + revision?: number; + expiration?: number; +}; export type RecordShareInfo = { - recordUid: string - userPermissions: RecordUserPermission[] - sharedFolderPermissions: RecordSharedFolderPermission[] -} + recordUid: string; + userPermissions: RecordUserPermission[]; + sharedFolderPermissions: RecordSharedFolderPermission[]; +}; function bytesToUid(bytes: Uint8Array | null | undefined): string | undefined { - return bytes && bytes.length > 0 ? webSafe64FromBytes(bytes) : undefined + return bytes && bytes.length > 0 ? webSafe64FromBytes(bytes) : undefined; } -function longToNumber(value: number | { toNumber: () => number } | null | undefined): number | undefined { - if (value == null) return undefined - return typeof value === 'number' ? value : value.toNumber() +function longToNumber( + value: number | { toNumber: () => number } | null | undefined, +): number | undefined { + if (value == null) return undefined; + return typeof value === "number" ? value : value.toNumber(); } -export async function getRecordShareInfo(auth: Auth, recordUid: string): Promise { - const msg = getRecordsDetailsMessage({ - clientTime: Date.now(), - recordUid: [normal64Bytes(recordUid)], - recordDetailsInclude: Records.RecordDetailsInclude.SHARE_ONLY, - }) - - let response: Records.IGetRecordDataWithAccessInfoResponse - try { - response = await auth.executeRest(msg) - } catch (err) { - throw new KeeperSdkError( - `Failed to fetch share info for ${recordUid}: ${extractErrorMessage(err)}` - ) - } - - const detail = response.recordDataWithAccessInfo?.[0] - if (!detail) return null - - const userPermissions: RecordUserPermission[] = (detail.userPermission ?? []).map((u) => ({ - username: u.username || '', - accountUid: bytesToUid(u.accountUid), - owner: !!u.owner, - shareAdmin: !!u.shareAdmin, - shareable: !!u.sharable, - editable: !!u.editable, - awaitingApproval: !!u.awaitingApproval, - expiration: longToNumber(u.expiration as number | null | undefined), - })) - - const sharedFolderPermissions: RecordSharedFolderPermission[] = (detail.sharedFolderPermission ?? []).map((s) => ({ - sharedFolderUid: bytesToUid(s.sharedFolderUid) ?? '', - resharable: !!s.resharable, - editable: !!s.editable, - revision: longToNumber(s.revision as number | null | undefined), - expiration: longToNumber(s.expiration as number | null | undefined), - })) - - return { recordUid, userPermissions, sharedFolderPermissions } +export async function getRecordShareInfo( + auth: Auth, + recordUid: string, +): Promise { + const msg = getRecordsDetailsMessage({ + clientTime: Date.now(), + recordUid: [normal64Bytes(recordUid)], + recordDetailsInclude: Records.RecordDetailsInclude.SHARE_ONLY, + }); + + let response: Records.IGetRecordDataWithAccessInfoResponse; + try { + response = await auth.executeRest(msg); + } catch (err) { + throw new KeeperSdkError( + `Failed to fetch share info for ${recordUid}: ${extractErrorMessage(err)}`, + ); + } + + const detail = response.recordDataWithAccessInfo?.[0]; + if (!detail) return null; + + const userPermissions: RecordUserPermission[] = ( + detail.userPermission ?? [] + ).map((u) => ({ + username: u.username || "", + accountUid: bytesToUid(u.accountUid), + owner: !!u.owner, + shareAdmin: !!u.shareAdmin, + shareable: !!u.sharable, + editable: !!u.editable, + awaitingApproval: !!u.awaitingApproval, + expiration: longToNumber(u.expiration as number | null | undefined), + })); + + const sharedFolderPermissions: RecordSharedFolderPermission[] = ( + detail.sharedFolderPermission ?? [] + ).map((s) => ({ + sharedFolderUid: bytesToUid(s.sharedFolderUid) ?? "", + resharable: !!s.resharable, + editable: !!s.editable, + revision: longToNumber(s.revision as number | null | undefined), + expiration: longToNumber(s.expiration as number | null | undefined), + })); + + return { recordUid, userPermissions, sharedFolderPermissions }; } diff --git a/KeeperSdk/src/storage/InMemoryStorage.ts b/KeeperSdk/src/storage/InMemoryStorage.ts index 1ddbe973..d8365af2 100644 --- a/KeeperSdk/src/storage/InMemoryStorage.ts +++ b/KeeperSdk/src/storage/InMemoryStorage.ts @@ -1,166 +1,180 @@ import type { - VaultStorage, - VaultStorageData, - VaultStorageKind, - VaultStorageResult, - Dependency, - Dependencies, - RemovedDependencies, - DRecord, -} from '@keeper-security/keeperapi' -import { webSafe64FromBytes } from '@keeper-security/keeperapi' -import { VaultObjectKind } from '../folders/folderHelpers' + VaultStorage, + VaultStorageData, + VaultStorageKind, + VaultStorageResult, + Dependency, + Dependencies, + RemovedDependencies, + DRecord, +} from "@keeper-security/keeperapi"; +import { webSafe64FromBytes } from "@keeper-security/keeperapi"; +import { VaultObjectKind } from "../folders/folderHelpers"; export class InMemoryStorage implements VaultStorage { - private keys = new Map() - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- KeyStorage.saveObject is unconstrained - private objects = new Map() - private store = new Map>() - private dependenciesByParent = new Map() - private arrayCache = new Map() - - public async getKeyBytes(keyId: string): Promise { - return this.keys.get(keyId) - } - - public async saveKeyBytes(keyId: string, key: Uint8Array): Promise { - this.keys.set(keyId, key) - } - - public async getObject(key: string): Promise { - return this.objects.get(key) as T | undefined - } - - public async saveObject(key: string, value: T): Promise { - this.objects.set(key, value) - } - - public async put(item: VaultStorageData): Promise { - const kind = item.kind - if (!kind) { - throw new Error('VaultStorageData missing kind') - } - if (!this.store.has(kind)) { - this.store.set(kind, new Map()) + private keys = new Map(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- KeyStorage.saveObject is unconstrained + private objects = new Map(); + private store = new Map>(); + private dependenciesByParent = new Map(); + private arrayCache = new Map(); + + public async getKeyBytes(keyId: string): Promise { + return this.keys.get(keyId); + } + + public async saveKeyBytes(keyId: string, key: Uint8Array): Promise { + this.keys.set(keyId, key); + } + + public async getObject(key: string): Promise { + return this.objects.get(key) as T | undefined; + } + + public async saveObject(key: string, value: T): Promise { + this.objects.set(key, value); + } + + public async put(item: VaultStorageData): Promise { + const kind = item.kind; + if (!kind) { + throw new Error("VaultStorageData missing kind"); + } + if (!this.store.has(kind)) { + this.store.set(kind, new Map()); + } + const uid = this.extractUid(item); + this.store.get(kind)!.set(uid, item); + this.arrayCache.delete(kind); + } + + public async get( + kind: T, + uid?: string, + ): Promise> { + const kindMap = this.store.get(kind); + if (!kindMap) return undefined as VaultStorageResult; + + if (uid) { + return kindMap.get(uid) as VaultStorageResult; + } + const first = kindMap.values().next(); + return (first.done ? undefined : first.value) as VaultStorageResult; + } + + public async delete( + kind: VaultStorageKind, + uid: string | Uint8Array, + ): Promise { + const uidStr = typeof uid === "string" ? uid : webSafe64FromBytes(uid); + this.store.get(kind)?.delete(uidStr); + this.arrayCache.delete(kind); + } + + public async clear(): Promise { + this.store.clear(); + this.keys.clear(); + this.objects.clear(); + this.dependenciesByParent.clear(); + this.arrayCache.clear(); + } + + public async getDependencies(uid: string): Promise { + return this.dependenciesByParent.get(uid); + } + + public async addDependencies(dependencies: Dependencies): Promise { + for (const [parentUid, children] of Object.entries(dependencies)) { + if (!this.dependenciesByParent.has(parentUid)) { + this.dependenciesByParent.set(parentUid, []); + } + const existing = this.dependenciesByParent.get(parentUid)!; + const seenChildUids = new Set( + existing.map((dependency) => dependency.uid), + ); + for (const child of children) { + if (!seenChildUids.has(child.uid)) { + existing.push(child); + seenChildUids.add(child.uid); } - const uid = this.extractUid(item) - this.store.get(kind)!.set(uid, item) - this.arrayCache.delete(kind) - } - - public async get(kind: T, uid?: string): Promise> { - const kindMap = this.store.get(kind) - if (!kindMap) return undefined as VaultStorageResult - - if (uid) { - return kindMap.get(uid) as VaultStorageResult - } - const first = kindMap.values().next() - return (first.done ? undefined : first.value) as VaultStorageResult - } - - public async delete(kind: VaultStorageKind, uid: string | Uint8Array): Promise { - const uidStr = typeof uid === 'string' ? uid : webSafe64FromBytes(uid) - this.store.get(kind)?.delete(uidStr) - this.arrayCache.delete(kind) - } - - public async clear(): Promise { - this.store.clear() - this.keys.clear() - this.objects.clear() - this.dependenciesByParent.clear() - this.arrayCache.clear() - } - - public async getDependencies(uid: string): Promise { - return this.dependenciesByParent.get(uid) - } - - public async addDependencies(dependencies: Dependencies): Promise { - for (const [parentUid, children] of Object.entries(dependencies)) { - if (!this.dependenciesByParent.has(parentUid)) { - this.dependenciesByParent.set(parentUid, []) - } - const existing = this.dependenciesByParent.get(parentUid)! - const seenChildUids = new Set(existing.map((dependency) => dependency.uid)) - for (const child of children) { - if (!seenChildUids.has(child.uid)) { - existing.push(child) - seenChildUids.add(child.uid) - } - } + } + } + } + + public async removeDependencies( + dependencies: RemovedDependencies, + ): Promise { + for (const [parentUid, children] of Object.entries(dependencies)) { + if (children === "*") { + this.dependenciesByParent.delete(parentUid); + } else { + const existing = this.dependenciesByParent.get(parentUid); + if (existing) { + const removeSet = children as Set; + this.dependenciesByParent.set( + parentUid, + existing.filter((dependency) => !removeSet.has(dependency.uid)), + ); } - } - - public async removeDependencies(dependencies: RemovedDependencies): Promise { - for (const [parentUid, children] of Object.entries(dependencies)) { - if (children === '*') { - this.dependenciesByParent.delete(parentUid) - } else { - const existing = this.dependenciesByParent.get(parentUid) - if (existing) { - const removeSet = children as Set - this.dependenciesByParent.set( - parentUid, - existing.filter((dependency) => !removeSet.has(dependency.uid)) - ) - } - } - } - } - - public getAll(kind: VaultStorageKind): T[] { - const cached = this.arrayCache.get(kind) - if (cached) return cached as T[] - - const kindMap = this.store.get(kind) - if (!kindMap) return [] - - const arr = Array.from(kindMap.values()) - this.arrayCache.set(kind, arr) - return arr as T[] - } - - public getRecords(): DRecord[] { - return this.getAll(VaultObjectKind.Record) - } - - public getByUid(kind: VaultStorageKind, uid: string): T | undefined { - return this.store.get(kind)?.get(uid) as T | undefined - } - - public getCount(kind: VaultStorageKind): number { - return this.store.get(kind)?.size ?? 0 - } - - private extractUid(item: VaultStorageData): string { - const record = item as VaultStorageData & { - uid?: string - token?: string - sharedFolderUid?: string - recordUid?: string - accountUid?: string | Uint8Array - teamUid?: string - } - const accountUidStr = - typeof record.accountUid === 'string' - ? record.accountUid - : record.accountUid instanceof Uint8Array - ? webSafe64FromBytes(record.accountUid) - : undefined - if (record.uid) return record.uid - if (record.token) return record.token - if (record.sharedFolderUid && record.recordUid) { - return `${record.sharedFolderUid}:${record.recordUid}` - } - if (record.sharedFolderUid && accountUidStr) { - return `${record.sharedFolderUid}:${accountUidStr}` - } - if (record.sharedFolderUid && record.teamUid) { - return `${record.sharedFolderUid}:${record.teamUid}` - } - if (item.kind === VaultObjectKind.User && accountUidStr) return accountUidStr - return '_singleton_' - } + } + } + } + + public getAll(kind: VaultStorageKind): T[] { + const cached = this.arrayCache.get(kind); + if (cached) return cached as T[]; + + const kindMap = this.store.get(kind); + if (!kindMap) return []; + + const arr = Array.from(kindMap.values()); + this.arrayCache.set(kind, arr); + return arr as T[]; + } + + public getRecords(): DRecord[] { + return this.getAll(VaultObjectKind.Record); + } + + public getByUid( + kind: VaultStorageKind, + uid: string, + ): T | undefined { + return this.store.get(kind)?.get(uid) as T | undefined; + } + + public getCount(kind: VaultStorageKind): number { + return this.store.get(kind)?.size ?? 0; + } + + private extractUid(item: VaultStorageData): string { + const record = item as VaultStorageData & { + uid?: string; + token?: string; + sharedFolderUid?: string; + recordUid?: string; + accountUid?: string | Uint8Array; + teamUid?: string; + }; + const accountUidStr = + typeof record.accountUid === "string" + ? record.accountUid + : record.accountUid instanceof Uint8Array + ? webSafe64FromBytes(record.accountUid) + : undefined; + if (record.uid) return record.uid; + if (record.token) return record.token; + if (record.sharedFolderUid && record.recordUid) { + return `${record.sharedFolderUid}:${record.recordUid}`; + } + if (record.sharedFolderUid && accountUidStr) { + return `${record.sharedFolderUid}:${accountUidStr}`; + } + if (record.sharedFolderUid && record.teamUid) { + return `${record.sharedFolderUid}:${record.teamUid}`; + } + if (item.kind === VaultObjectKind.User && accountUidStr) + return accountUidStr; + return "_singleton_"; + } } diff --git a/KeeperSdk/src/teams/TeamManager.ts b/KeeperSdk/src/teams/TeamManager.ts index 4b8ad230..e39bcc61 100644 --- a/KeeperSdk/src/teams/TeamManager.ts +++ b/KeeperSdk/src/teams/TeamManager.ts @@ -1,125 +1,143 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; import { - formatTeamsTable, - listTeams, - renderTeamsAsciiTable, - type FormatTeamsTableOptions, - type FormattedTeamsTable, - type ListTeamRow, - type ListTeamsOptions, -} from './listTeams' + formatTeamsTable, + listTeams, + renderTeamsAsciiTable, + type FormatTeamsTableOptions, + type FormattedTeamsTable, + type ListTeamRow, + type ListTeamsOptions, +} from "./listTeams"; import { - formatTeamView, - teamViewTable, - viewTeam, - type FormatTeamViewOptions, - type FormattedTeamViewTable, - type TeamView, -} from './viewTeam' + formatTeamView, + teamViewTable, + viewTeam, + type FormatTeamViewOptions, + type FormattedTeamViewTable, + type TeamView, +} from "./viewTeam"; import { - addTeams, - formatAddTeamResult, - renderAddTeamAsciiTable, - type AddTeamInput, - type AddTeamResult, - type FormatAddTeamResultOptions, - type FormattedAddTeamTable, -} from './addTeam' + addTeams, + formatAddTeamResult, + renderAddTeamAsciiTable, + type AddTeamInput, + type AddTeamResult, + type FormatAddTeamResultOptions, + type FormattedAddTeamTable, +} from "./addTeam"; import { - formatUpdateTeamResult, - renderUpdateTeamAsciiTable, - updateTeams, - type FormattedUpdateTeamTable, - type UpdateTeamInput, - type UpdateTeamResult, -} from './updateTeam' + formatUpdateTeamResult, + renderUpdateTeamAsciiTable, + updateTeams, + type FormattedUpdateTeamTable, + type UpdateTeamInput, + type UpdateTeamResult, +} from "./updateTeam"; import { - deleteTeams, - formatDeleteTeamResult, - renderDeleteTeamAsciiTable, - type DeleteTeamInput, - type DeleteTeamResult, - type FormattedDeleteTeamTable, -} from './deleteTeam' + deleteTeams, + formatDeleteTeamResult, + renderDeleteTeamAsciiTable, + type DeleteTeamInput, + type DeleteTeamResult, + type FormattedDeleteTeamTable, +} from "./deleteTeam"; -export type AuthProvider = () => Auth +export type AuthProvider = () => Auth; export class TeamManager { - private readonly authProvider: AuthProvider - - constructor(authProvider: AuthProvider) { - this.authProvider = authProvider - } - - public async listTeams(options: ListTeamsOptions = {}): Promise { - return listTeams(this.requireAuth(), options) - } - - public formatTeamsTable(rows: ListTeamRow[], options: FormatTeamsTableOptions = {}): FormattedTeamsTable { - return formatTeamsTable(rows, options) - } - - public renderTeamsAsciiTable(table: FormattedTeamsTable, options: { minColWidth?: number } = {}): string { - return renderTeamsAsciiTable(table, options) - } - - public async viewTeam(identifier: string): Promise { - return viewTeam(this.requireAuth(), identifier) - } - - public formatTeamView(view: TeamView, options: FormatTeamViewOptions = {}): FormattedTeamViewTable { - return formatTeamView(view, options) - } - - public teamViewTable(table: FormattedTeamViewTable): string { - return teamViewTable(table) - } - - public async addTeams(input: AddTeamInput): Promise { - return addTeams(this.requireAuth(), input) - } - - public formatAddTeamResult( - result: AddTeamResult, - options: FormatAddTeamResultOptions = {} - ): FormattedAddTeamTable { - return formatAddTeamResult(result, options) - } - - public renderAddTeamAsciiTable(table: FormattedAddTeamTable): string { - return renderAddTeamAsciiTable(table) - } - - public async updateTeams(input: UpdateTeamInput): Promise { - return updateTeams(this.requireAuth(), input) - } - - public formatUpdateTeamResult(result: UpdateTeamResult): FormattedUpdateTeamTable { - return formatUpdateTeamResult(result) - } - - public renderUpdateTeamAsciiTable(table: FormattedUpdateTeamTable): string { - return renderUpdateTeamAsciiTable(table) - } - - public async deleteTeams(input: DeleteTeamInput): Promise { - return deleteTeams(this.requireAuth(), input) - } - - public formatDeleteTeamResult(result: DeleteTeamResult): FormattedDeleteTeamTable { - return formatDeleteTeamResult(result) - } - - public renderDeleteTeamAsciiTable(table: FormattedDeleteTeamTable): string { - return renderDeleteTeamAsciiTable(table) - } - - private requireAuth(): Auth { - const auth = this.authProvider() - if (!auth) { - throw new KeeperSdkError('You are not logged in. Please log in first.', ResultCodes.NOT_LOGGED_IN) - } - return auth - } + private readonly authProvider: AuthProvider; + + constructor(authProvider: AuthProvider) { + this.authProvider = authProvider; + } + + public async listTeams( + options: ListTeamsOptions = {}, + ): Promise { + return listTeams(this.requireAuth(), options); + } + + public formatTeamsTable( + rows: ListTeamRow[], + options: FormatTeamsTableOptions = {}, + ): FormattedTeamsTable { + return formatTeamsTable(rows, options); + } + + public renderTeamsAsciiTable( + table: FormattedTeamsTable, + options: { minColWidth?: number } = {}, + ): string { + return renderTeamsAsciiTable(table, options); + } + + public async viewTeam(identifier: string): Promise { + return viewTeam(this.requireAuth(), identifier); + } + + public formatTeamView( + view: TeamView, + options: FormatTeamViewOptions = {}, + ): FormattedTeamViewTable { + return formatTeamView(view, options); + } + + public teamViewTable(table: FormattedTeamViewTable): string { + return teamViewTable(table); + } + + public async addTeams(input: AddTeamInput): Promise { + return addTeams(this.requireAuth(), input); + } + + public formatAddTeamResult( + result: AddTeamResult, + options: FormatAddTeamResultOptions = {}, + ): FormattedAddTeamTable { + return formatAddTeamResult(result, options); + } + + public renderAddTeamAsciiTable(table: FormattedAddTeamTable): string { + return renderAddTeamAsciiTable(table); + } + + public async updateTeams(input: UpdateTeamInput): Promise { + return updateTeams(this.requireAuth(), input); + } + + public formatUpdateTeamResult( + result: UpdateTeamResult, + ): FormattedUpdateTeamTable { + return formatUpdateTeamResult(result); + } + + public renderUpdateTeamAsciiTable(table: FormattedUpdateTeamTable): string { + return renderUpdateTeamAsciiTable(table); + } + + public async deleteTeams(input: DeleteTeamInput): Promise { + return deleteTeams(this.requireAuth(), input); + } + + public formatDeleteTeamResult( + result: DeleteTeamResult, + ): FormattedDeleteTeamTable { + return formatDeleteTeamResult(result); + } + + public renderDeleteTeamAsciiTable(table: FormattedDeleteTeamTable): string { + return renderDeleteTeamAsciiTable(table); + } + + private requireAuth(): Auth { + const auth = this.authProvider(); + if (!auth) { + throw new KeeperSdkError( + "You are not logged in. Please log in first.", + ResultCodes.NOT_LOGGED_IN, + ); + } + return auth; + } } diff --git a/KeeperSdk/src/teams/addTeam.ts b/KeeperSdk/src/teams/addTeam.ts index 732c7912..39b9b7b8 100644 --- a/KeeperSdk/src/teams/addTeam.ts +++ b/KeeperSdk/src/teams/addTeam.ts @@ -1,490 +1,525 @@ import { - encryptForStorage, - encryptKey, - generateEncryptionKey, - generateUid, - platform, - webSafe64FromBytes, - type Auth, - type KeeperResponse, - type RestCommand, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' + encryptForStorage, + encryptKey, + generateEncryptionKey, + generateUid, + platform, + webSafe64FromBytes, + type Auth, + type KeeperResponse, + type RestCommand, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseNode, - type EnterpriseQueuedTeamRecord, - type EnterpriseTeamRecord, -} from './enterpriseData' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseNode, + type EnterpriseQueuedTeamRecord, + type EnterpriseTeamRecord, +} from "./enterpriseData"; import { - applyDecryptedNodeNames, - applyEnterpriseNameToRoot, - NODE_PATH_SEPARATOR, - parentNeedsNameLookup, - resolveParentNode, - TEAM_TABLE_HEADERS, - validateTeamName, -} from './teamUtils' - -const TEAM_ADD_COMMAND = 'team_add' + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + NODE_PATH_SEPARATOR, + parentNeedsNameLookup, + resolveParentNode, + TEAM_TABLE_HEADERS, + validateTeamName, +} from "./teamUtils"; + +const TEAM_ADD_COMMAND = "team_add"; const ADD_TEAM_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Teams, - EnterpriseDataInclude.QueuedTeams, -] + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.QueuedTeams, +]; export enum TeamRestriction { - On = 'on', - Off = 'off', + On = "on", + Off = "off", } -export type TeamRestrictionInput = TeamRestriction | `${TeamRestriction}` | null | undefined +export type TeamRestrictionInput = + | TeamRestriction + | `${TeamRestriction}` + | null + | undefined; export enum AddTeamStatus { - Created = 'created', - Skipped = 'skipped', - Failed = 'failed', + Created = "created", + Skipped = "skipped", + Failed = "failed", } export enum AddTeamSkipReason { - AlreadyExistsInParent = 'already_exists_in_parent', - ExistsElsewhereDeclined = 'exists_elsewhere_declined', + AlreadyExistsInParent = "already_exists_in_parent", + ExistsElsewhereDeclined = "exists_elsewhere_declined", } export enum AddTeamSourceKind { - NewName = 'new_name', - QueuedTeam = 'queued_team', + NewName = "new_name", + QueuedTeam = "queued_team", } export type AddTeamConflictPrompt = { - teamName: string - parentNodeId: number - parentNodeName: string - existingTeamUid: string - existingTeamName: string - existingNodeId: number - existingNodeName: string -} - -export type AddTeamConfirm = (prompt: AddTeamConflictPrompt) => boolean | Promise + teamName: string; + parentNodeId: number; + parentNodeName: string; + existingTeamUid: string; + existingTeamName: string; + existingNodeId: number; + existingNodeName: string; +}; + +export type AddTeamConfirm = ( + prompt: AddTeamConflictPrompt, +) => boolean | Promise; export type AddTeamInput = { - teams: string[] - parent?: string | number | null - restrictEdit?: TeamRestrictionInput - restrictShare?: TeamRestrictionInput - restrictView?: TeamRestrictionInput - force?: boolean - confirm?: AddTeamConfirm -} + teams: string[]; + parent?: string | number | null; + restrictEdit?: TeamRestrictionInput; + restrictShare?: TeamRestrictionInput; + restrictView?: TeamRestrictionInput; + force?: boolean; + confirm?: AddTeamConfirm; +}; export type AddTeamItemResult = { - teamUid: string - teamName: string - nodeId: number - source: AddTeamSourceKind - status: AddTeamStatus - skipReason?: AddTeamSkipReason - message?: string -} + teamUid: string; + teamName: string; + nodeId: number; + source: AddTeamSourceKind; + status: AddTeamStatus; + skipReason?: AddTeamSkipReason; + message?: string; +}; export type AddTeamResult = { - success: boolean - parentNodeId: number - parentNodeName: string - items: AddTeamItemResult[] - created: number - skipped: number - failed: number -} + success: boolean; + parentNodeId: number; + parentNodeName: string; + items: AddTeamItemResult[]; + created: number; + skipped: number; + failed: number; +}; type ResolvedRequest = - | { kind: AddTeamSourceKind.NewName; teamUid: string; teamName: string } - | { - kind: AddTeamSourceKind.QueuedTeam - teamUid: string - teamName: string - queued: EnterpriseQueuedTeamRecord - } + | { kind: AddTeamSourceKind.NewName; teamUid: string; teamName: string } + | { + kind: AddTeamSourceKind.QueuedTeam; + teamUid: string; + teamName: string; + queued: EnterpriseQueuedTeamRecord; + }; type TeamAddRequestPayload = { - team_uid: string - team_name: string - node_id: number - public_key: string - private_key: string - team_key: string - encrypted_team_key: string - ecc_public_key: string - ecc_private_key: string - restrict_edit: boolean - restrict_share: boolean - restrict_view: boolean - manage_only: boolean -} - -export async function addTeams(auth: Auth, input: AddTeamInput): Promise { - const requestedNames = (input.teams || []) - .map((value) => (typeof value === 'string' ? value.trim() : '')) - .filter((value) => value.length > 0) - - if (requestedNames.length === 0) { - throw new KeeperSdkError('No teams to add.', ResultCodes.NO_TEAMS_TO_ADD) + team_uid: string; + team_name: string; + node_id: number; + public_key: string; + private_key: string; + team_key: string; + encrypted_team_key: string; + ecc_public_key: string; + ecc_private_key: string; + restrict_edit: boolean; + restrict_share: boolean; + restrict_view: boolean; + manage_only: boolean; +}; + +export async function addTeams( + auth: Auth, + input: AddTeamInput, +): Promise { + const requestedNames = (input.teams || []) + .map((value) => (typeof value === "string" ? value.trim() : "")) + .filter((value) => value.length > 0); + + if (requestedNames.length === 0) { + throw new KeeperSdkError("No teams to add.", ResultCodes.NO_TEAMS_TO_ADD); + } + + for (const name of requestedNames) { + validateTeamName(name); + } + + const force = input.force === true; + const restrictEdit = resolveRestriction(input.restrictEdit); + const restrictShare = resolveRestriction(input.restrictShare); + const restrictView = resolveRestriction(input.restrictView); + + const needsNameLookup = parentNeedsNameLookup(input.parent ?? null); + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(ADD_TEAM_INCLUDES); + const nodes = response.nodes || []; + if (needsNameLookup) { + const displayNames = await enterpriseData.getDisplayNames(); + applyDecryptedNodeNames(nodes, displayNames.nodes); + await enterpriseData.decryptNodeNames(nodes); + } + applyEnterpriseNameToRoot(nodes, response.enterprise_name); + + const parentNode = resolveParentNode(nodes, input.parent ?? null); + const parentNodeId = parentNode.node_id; + const parentNodeName = nodePathOrFallback(nodes, parentNode); + + const existingTeams = response.teams || []; + const queuedTeams = response.queued_teams || []; + const queuedByUid = buildQueuedByUid(queuedTeams); + const queuedByLowerName = buildQueuedByLowerName(queuedTeams); + const teamsByLowerName = buildTeamsByLowerName(existingTeams); + + const queuedRequests: ResolvedRequest[] = []; + const newRequests: ResolvedRequest[] = []; + const items: AddTeamItemResult[] = []; + const seenLowerNames = new Set(); + + for (const raw of requestedNames) { + const lower = raw.toLowerCase(); + if (seenLowerNames.has(lower)) continue; + seenLowerNames.add(lower); + + const queued = queuedByUid.get(raw) || queuedByLowerName.get(lower); + if (queued) { + queuedRequests.push({ + kind: AddTeamSourceKind.QueuedTeam, + teamUid: queued.team_uid, + teamName: queued.name || raw, + queued, + }); + continue; } - for (const name of requestedNames) { - validateTeamName(name) + const conflicts = teamsByLowerName.get(lower) || []; + const sameParent = conflicts.find((team) => team.node_id === parentNodeId); + if (sameParent) { + items.push({ + teamUid: sameParent.team_uid, + teamName: sameParent.name || raw, + nodeId: sameParent.node_id, + source: AddTeamSourceKind.NewName, + status: AddTeamStatus.Skipped, + skipReason: AddTeamSkipReason.AlreadyExistsInParent, + message: `Team "${sameParent.name || raw}" already exists in parent node ${parentNodeId}.`, + }); + continue; } - const force = input.force === true - const restrictEdit = resolveRestriction(input.restrictEdit) - const restrictShare = resolveRestriction(input.restrictShare) - const restrictView = resolveRestriction(input.restrictView) - - const needsNameLookup = parentNeedsNameLookup(input.parent ?? null) - - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(ADD_TEAM_INCLUDES) - const nodes = response.nodes || [] - if (needsNameLookup) { - const displayNames = await enterpriseData.getDisplayNames() - applyDecryptedNodeNames(nodes, displayNames.nodes) - await enterpriseData.decryptNodeNames(nodes) - } - applyEnterpriseNameToRoot(nodes, response.enterprise_name) - - const parentNode = resolveParentNode(nodes, input.parent ?? null) - const parentNodeId = parentNode.node_id - const parentNodeName = nodePathOrFallback(nodes, parentNode) - - const existingTeams = response.teams || [] - const queuedTeams = response.queued_teams || [] - const queuedByUid = buildQueuedByUid(queuedTeams) - const queuedByLowerName = buildQueuedByLowerName(queuedTeams) - const teamsByLowerName = buildTeamsByLowerName(existingTeams) - - const queuedRequests: ResolvedRequest[] = [] - const newRequests: ResolvedRequest[] = [] - const items: AddTeamItemResult[] = [] - const seenLowerNames = new Set() - - for (const raw of requestedNames) { - const lower = raw.toLowerCase() - if (seenLowerNames.has(lower)) continue - seenLowerNames.add(lower) - - const queued = queuedByUid.get(raw) || queuedByLowerName.get(lower) - if (queued) { - queuedRequests.push({ - kind: AddTeamSourceKind.QueuedTeam, - teamUid: queued.team_uid, - teamName: queued.name || raw, - queued, - }) - continue - } - - const conflicts = teamsByLowerName.get(lower) || [] - const sameParent = conflicts.find((team) => team.node_id === parentNodeId) - if (sameParent) { - items.push({ - teamUid: sameParent.team_uid, - teamName: sameParent.name || raw, - nodeId: sameParent.node_id, - source: AddTeamSourceKind.NewName, - status: AddTeamStatus.Skipped, - skipReason: AddTeamSkipReason.AlreadyExistsInParent, - message: `Team "${sameParent.name || raw}" already exists in parent node ${parentNodeId}.`, - }) - continue - } - - if (conflicts.length > 0 && !force) { - const confirmed = await confirmCrossNode(input.confirm, conflicts[0], { - teamName: raw, - parentNodeId, - parentNodeName, - existingNodeName: nodePathById(nodes, conflicts[0].node_id), - }) - if (!confirmed) { - items.push({ - teamUid: conflicts[0].team_uid, - teamName: raw, - nodeId: conflicts[0].node_id, - source: AddTeamSourceKind.NewName, - status: AddTeamStatus.Skipped, - skipReason: AddTeamSkipReason.ExistsElsewhereDeclined, - message: `Team "${raw}" already exists in node ${conflicts[0].node_id}; creation declined.`, - }) - continue - } - } - - newRequests.push({ - kind: AddTeamSourceKind.NewName, - teamUid: generateUid(), - teamName: raw, - }) - } - - const planned = [...queuedRequests, ...newRequests] - if (planned.length === 0) { - return finalizeResult(items, parentNodeId, parentNodeName) - } - - const treeKey = await enterpriseData.getTreeKey() - if (!treeKey) { - throw new KeeperSdkError( - 'Enterprise tree key is unavailable. The current user may not have permission to administer teams.', - ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE - ) - } - const dataKey = auth.dataKey - if (!dataKey) { - throw new KeeperSdkError( - 'Data key not available. Ensure you are logged in.', - ResultCodes.DATA_KEY_MISSING - ) + if (conflicts.length > 0 && !force) { + const confirmed = await confirmCrossNode(input.confirm, conflicts[0], { + teamName: raw, + parentNodeId, + parentNodeName, + existingNodeName: nodePathById(nodes, conflicts[0].node_id), + }); + if (!confirmed) { + items.push({ + teamUid: conflicts[0].team_uid, + teamName: raw, + nodeId: conflicts[0].node_id, + source: AddTeamSourceKind.NewName, + status: AddTeamStatus.Skipped, + skipReason: AddTeamSkipReason.ExistsElsewhereDeclined, + message: `Team "${raw}" already exists in node ${conflicts[0].node_id}; creation declined.`, + }); + continue; + } } - for (const request of planned) { - try { - await sendTeamAdd(auth, request, parentNodeId, dataKey, treeKey, { - restrictEdit, - restrictShare, - restrictView, - }) - items.push({ - teamUid: request.teamUid, - teamName: request.teamName, - nodeId: parentNodeId, - source: request.kind, - status: AddTeamStatus.Created, - }) - } catch (err) { - items.push({ - teamUid: request.teamUid, - teamName: request.teamName, - nodeId: parentNodeId, - source: request.kind, - status: AddTeamStatus.Failed, - message: extractErrorMessage(err), - }) - } + newRequests.push({ + kind: AddTeamSourceKind.NewName, + teamUid: generateUid(), + teamName: raw, + }); + } + + const planned = [...queuedRequests, ...newRequests]; + if (planned.length === 0) { + return finalizeResult(items, parentNodeId, parentNodeName); + } + + const treeKey = await enterpriseData.getTreeKey(); + if (!treeKey) { + throw new KeeperSdkError( + "Enterprise tree key is unavailable. The current user may not have permission to administer teams.", + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE, + ); + } + const dataKey = auth.dataKey; + if (!dataKey) { + throw new KeeperSdkError( + "Data key not available. Ensure you are logged in.", + ResultCodes.DATA_KEY_MISSING, + ); + } + + for (const request of planned) { + try { + await sendTeamAdd(auth, request, parentNodeId, dataKey, treeKey, { + restrictEdit, + restrictShare, + restrictView, + }); + items.push({ + teamUid: request.teamUid, + teamName: request.teamName, + nodeId: parentNodeId, + source: request.kind, + status: AddTeamStatus.Created, + }); + } catch (err) { + items.push({ + teamUid: request.teamUid, + teamName: request.teamName, + nodeId: parentNodeId, + source: request.kind, + status: AddTeamStatus.Failed, + message: extractErrorMessage(err), + }); } + } - return finalizeResult(items, parentNodeId, parentNodeName) + return finalizeResult(items, parentNodeId, parentNodeName); } function resolveRestriction(input: TeamRestrictionInput): boolean { - if (input === undefined || input === null) return false - return String(input).toLowerCase() === TeamRestriction.On + if (input === undefined || input === null) return false; + return String(input).toLowerCase() === TeamRestriction.On; } -function nodePathOrFallback(nodes: EnterpriseNode[], node: EnterpriseNode): string { - const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { - omitRoot: false, - separator: NODE_PATH_SEPARATOR, - }) - return path || (node.displayName || '').trim() || String(node.node_id) +function nodePathOrFallback( + nodes: EnterpriseNode[], + node: EnterpriseNode, +): string { + const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }); + return path || (node.displayName || "").trim() || String(node.node_id); } function nodePathById(nodes: EnterpriseNode[], nodeId: number): string { - return ( - EnterpriseDataManager.getNodePath(nodes, nodeId, { - omitRoot: false, - separator: NODE_PATH_SEPARATOR, - }) || String(nodeId) - ) + return ( + EnterpriseDataManager.getNodePath(nodes, nodeId, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }) || String(nodeId) + ); } -function buildTeamsByLowerName(teams: EnterpriseTeamRecord[]): Map { - const map = new Map() - for (const team of teams) { - const key = (team.name || '').trim().toLowerCase() - if (!key) continue - const existing = map.get(key) - if (existing) existing.push(team) - else map.set(key, [team]) - } - return map +function buildTeamsByLowerName( + teams: EnterpriseTeamRecord[], +): Map { + const map = new Map(); + for (const team of teams) { + const key = (team.name || "").trim().toLowerCase(); + if (!key) continue; + const existing = map.get(key); + if (existing) existing.push(team); + else map.set(key, [team]); + } + return map; } -function buildQueuedByUid(queued: EnterpriseQueuedTeamRecord[]): Map { - const map = new Map() - for (const team of queued) { - if (team.team_uid) map.set(team.team_uid, team) - } - return map +function buildQueuedByUid( + queued: EnterpriseQueuedTeamRecord[], +): Map { + const map = new Map(); + for (const team of queued) { + if (team.team_uid) map.set(team.team_uid, team); + } + return map; } function buildQueuedByLowerName( - queued: EnterpriseQueuedTeamRecord[] + queued: EnterpriseQueuedTeamRecord[], ): Map { - const map = new Map() - for (const team of queued) { - const key = (team.name || '').trim().toLowerCase() - if (!key) continue - if (!map.has(key)) map.set(key, team) - } - return map + const map = new Map(); + for (const team of queued) { + const key = (team.name || "").trim().toLowerCase(); + if (!key) continue; + if (!map.has(key)) map.set(key, team); + } + return map; } async function confirmCrossNode( - confirm: AddTeamConfirm | undefined, - existing: EnterpriseTeamRecord, - context: { - teamName: string - parentNodeId: number - parentNodeName: string - existingNodeName: string - } + confirm: AddTeamConfirm | undefined, + existing: EnterpriseTeamRecord, + context: { + teamName: string; + parentNodeId: number; + parentNodeName: string; + existingNodeName: string; + }, ): Promise { - if (!confirm) return false - const result = await confirm({ - teamName: context.teamName, - parentNodeId: context.parentNodeId, - parentNodeName: context.parentNodeName, - existingTeamUid: existing.team_uid, - existingTeamName: existing.name || context.teamName, - existingNodeId: existing.node_id, - existingNodeName: context.existingNodeName, - }) - return result === true + if (!confirm) return false; + const result = await confirm({ + teamName: context.teamName, + parentNodeId: context.parentNodeId, + parentNodeName: context.parentNodeName, + existingTeamUid: existing.team_uid, + existingTeamName: existing.name || context.teamName, + existingNodeId: existing.node_id, + existingNodeName: context.existingNodeName, + }); + return result === true; } async function sendTeamAdd( - auth: Auth, - request: ResolvedRequest, - parentNodeId: number, - dataKey: Uint8Array, - treeKey: Uint8Array, - restrictions: { restrictEdit: boolean; restrictShare: boolean; restrictView: boolean } + auth: Auth, + request: ResolvedRequest, + parentNodeId: number, + dataKey: Uint8Array, + treeKey: Uint8Array, + restrictions: { + restrictEdit: boolean; + restrictShare: boolean; + restrictView: boolean; + }, ): Promise { - const teamKeyBytes = generateEncryptionKey() - const [rsaKeyPair, eccKeyPair] = await Promise.all([ - platform.generateRSAKeyPair(), - platform.generateECKeyPair(), - ]) - const encryptedPrivateKey = await encryptForStorage(rsaKeyPair.privateKey, teamKeyBytes) - const encryptedEccPrivateKey = await encryptKey(eccKeyPair.privateKey, teamKeyBytes) - const encryptedTeamKeyByDataKey = await encryptForStorage(teamKeyBytes, dataKey) - const encryptedTeamKeyByTreeKey = await encryptKey(teamKeyBytes, treeKey) - - const payload: TeamAddRequestPayload = { - team_uid: request.teamUid, - team_name: request.teamName, - node_id: parentNodeId, - public_key: webSafe64FromBytes(rsaKeyPair.publicKey), - private_key: encryptedPrivateKey, - team_key: encryptedTeamKeyByDataKey, - encrypted_team_key: encryptedTeamKeyByTreeKey, - ecc_public_key: webSafe64FromBytes(eccKeyPair.publicKey), - ecc_private_key: encryptedEccPrivateKey, - restrict_edit: restrictions.restrictEdit, - restrict_share: restrictions.restrictShare, - restrict_view: restrictions.restrictView, - manage_only: true, - } - - const command: RestCommand = { - baseRequest: { command: TEAM_ADD_COMMAND }, - request: payload, - authorization: {}, - } - - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `team_add failed for "${request.teamName}" (uid=${request.teamUid})`, - response.result_code || ResultCodes.TEAM_ADD_FAILED - ) - } + const teamKeyBytes = generateEncryptionKey(); + const [rsaKeyPair, eccKeyPair] = await Promise.all([ + platform.generateRSAKeyPair(), + platform.generateECKeyPair(), + ]); + const encryptedPrivateKey = await encryptForStorage( + rsaKeyPair.privateKey, + teamKeyBytes, + ); + const encryptedEccPrivateKey = await encryptKey( + eccKeyPair.privateKey, + teamKeyBytes, + ); + const encryptedTeamKeyByDataKey = await encryptForStorage( + teamKeyBytes, + dataKey, + ); + const encryptedTeamKeyByTreeKey = await encryptKey(teamKeyBytes, treeKey); + + const payload: TeamAddRequestPayload = { + team_uid: request.teamUid, + team_name: request.teamName, + node_id: parentNodeId, + public_key: webSafe64FromBytes(rsaKeyPair.publicKey), + private_key: encryptedPrivateKey, + team_key: encryptedTeamKeyByDataKey, + encrypted_team_key: encryptedTeamKeyByTreeKey, + ecc_public_key: webSafe64FromBytes(eccKeyPair.publicKey), + ecc_private_key: encryptedEccPrivateKey, + restrict_edit: restrictions.restrictEdit, + restrict_share: restrictions.restrictShare, + restrict_view: restrictions.restrictView, + manage_only: true, + }; + + const command: RestCommand = { + baseRequest: { command: TEAM_ADD_COMMAND }, + request: payload, + authorization: {}, + }; + + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `team_add failed for "${request.teamName}" (uid=${request.teamUid})`, + response.result_code || ResultCodes.TEAM_ADD_FAILED, + ); + } } function finalizeResult( - items: AddTeamItemResult[], - parentNodeId: number, - parentNodeName: string + items: AddTeamItemResult[], + parentNodeId: number, + parentNodeName: string, ): AddTeamResult { - const created = items.filter((item) => item.status === AddTeamStatus.Created).length - const skipped = items.filter((item) => item.status === AddTeamStatus.Skipped).length - const failed = items.filter((item) => item.status === AddTeamStatus.Failed).length - return { - success: failed === 0 && created > 0, - parentNodeId, - parentNodeName, - items, - created, - skipped, - failed, - } + const created = items.filter( + (item) => item.status === AddTeamStatus.Created, + ).length; + const skipped = items.filter( + (item) => item.status === AddTeamStatus.Skipped, + ).length; + const failed = items.filter( + (item) => item.status === AddTeamStatus.Failed, + ).length; + return { + success: failed === 0 && created > 0, + parentNodeId, + parentNodeName, + items, + created, + skipped, + failed, + }; } export type FormatAddTeamResultOptions = { - showSkipped?: boolean -} + showSkipped?: boolean; +}; export type FormattedAddTeamRow = { - status: string - name: string - teamUid: string - nodeId: number - detail: string -} + status: string; + name: string; + teamUid: string; + nodeId: number; + detail: string; +}; export type FormattedAddTeamTable = { - headers: string[] - rows: string[][] - parentNodeName: string - summary: string -} + headers: string[]; + rows: string[][]; + parentNodeName: string; + summary: string; +}; export function formatAddTeamResult( - result: AddTeamResult, - options: FormatAddTeamResultOptions = {} + result: AddTeamResult, + options: FormatAddTeamResultOptions = {}, ): FormattedAddTeamTable { - const showSkipped = options.showSkipped !== false - const visible = result.items.filter( - (item) => showSkipped || item.status !== AddTeamStatus.Skipped - ) - - const rows = visible.map((item, index) => [ - String(index + 1), - item.status, - item.teamName, - item.teamUid, - String(item.nodeId), - item.message || (item.skipReason ? item.skipReason : ''), - ]) - - return { - headers: [...TEAM_TABLE_HEADERS], - rows, - parentNodeName: result.parentNodeName, - summary: `Created: ${result.created} Skipped: ${result.skipped} Failed: ${result.failed}`, - } + const showSkipped = options.showSkipped !== false; + const visible = result.items.filter( + (item) => showSkipped || item.status !== AddTeamStatus.Skipped, + ); + + const rows = visible.map((item, index) => [ + String(index + 1), + item.status, + item.teamName, + item.teamUid, + String(item.nodeId), + item.message || (item.skipReason ? item.skipReason : ""), + ]); + + return { + headers: [...TEAM_TABLE_HEADERS], + rows, + parentNodeName: result.parentNodeName, + summary: `Created: ${result.created} Skipped: ${result.skipped} Failed: ${result.failed}`, + }; } export function renderAddTeamAsciiTable(table: FormattedAddTeamTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell + ' '.repeat(Math.max(0, widths[columnIndex] - cell.length)) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [] - lines.push(`Parent: ${table.parentNodeName}`) - lines.push(formatRow(headers)) - lines.push(formatRow(widths.map((width) => '-'.repeat(width)))) - for (const row of rows) lines.push(formatRow(row)) - lines.push(table.summary) - return lines.join('\n') + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell + " ".repeat(Math.max(0, widths[columnIndex] - cell.length)); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = []; + lines.push(`Parent: ${table.parentNodeName}`); + lines.push(formatRow(headers)); + lines.push(formatRow(widths.map((width) => "-".repeat(width)))); + for (const row of rows) lines.push(formatRow(row)); + lines.push(table.summary); + return lines.join("\n"); } diff --git a/KeeperSdk/src/teams/deleteTeam.ts b/KeeperSdk/src/teams/deleteTeam.ts index 704969e3..4ffa709a 100644 --- a/KeeperSdk/src/teams/deleteTeam.ts +++ b/KeeperSdk/src/teams/deleteTeam.ts @@ -1,154 +1,171 @@ -import type { Auth, KeeperResponse, RestCommand } from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' -import { - EnterpriseDataInclude, - EnterpriseDataManager, -} from './enterpriseData' -import { resolveExistingTeams, TEAM_TABLE_HEADERS } from './teamUtils' - -const TEAM_DELETE_COMMAND = 'team_delete' -const DELETE_TEAM_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Teams] +import type { + Auth, + KeeperResponse, + RestCommand, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; +import { EnterpriseDataInclude, EnterpriseDataManager } from "./enterpriseData"; +import { resolveExistingTeams, TEAM_TABLE_HEADERS } from "./teamUtils"; + +const TEAM_DELETE_COMMAND = "team_delete"; +const DELETE_TEAM_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Teams, +]; export enum DeleteTeamStatus { - Deleted = 'deleted', - Failed = 'failed', + Deleted = "deleted", + Failed = "failed", } export type DeleteTeamInput = { - teams: string[] -} + teams: string[]; +}; export type DeleteTeamItemResult = { - teamUid: string - teamName: string - nodeId: number - status: DeleteTeamStatus - message?: string -} + teamUid: string; + teamName: string; + nodeId: number; + status: DeleteTeamStatus; + message?: string; +}; export type DeleteTeamResult = { - success: boolean - items: DeleteTeamItemResult[] - deleted: number - failed: number -} + success: boolean; + items: DeleteTeamItemResult[]; + deleted: number; + failed: number; +}; type TeamDeleteRequestPayload = { - team_uid: string -} - -export async function deleteTeams(auth: Auth, input: DeleteTeamInput): Promise { - const requestedIdentifiers = (input.teams || []) - .map((value) => (typeof value === 'string' ? value.trim() : '')) - .filter((value) => value.length > 0) - - if (requestedIdentifiers.length === 0) { - throw new KeeperSdkError('No teams to delete.', ResultCodes.NO_TEAMS_TO_DELETE) - } - - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(DELETE_TEAM_INCLUDES) - const allTeams = response.teams || [] - const resolvedTeams = resolveExistingTeams(allTeams, requestedIdentifiers) - - const items: DeleteTeamItemResult[] = [] - for (const team of resolvedTeams) { - try { - await sendTeamDelete(auth, team.team_uid) - items.push({ - teamUid: team.team_uid, - teamName: team.name || '', - nodeId: team.node_id, - status: DeleteTeamStatus.Deleted, - }) - } catch (err) { - items.push({ - teamUid: team.team_uid, - teamName: team.name || '', - nodeId: team.node_id, - status: DeleteTeamStatus.Failed, - message: extractErrorMessage(err), - }) - } + team_uid: string; +}; + +export async function deleteTeams( + auth: Auth, + input: DeleteTeamInput, +): Promise { + const requestedIdentifiers = (input.teams || []) + .map((value) => (typeof value === "string" ? value.trim() : "")) + .filter((value) => value.length > 0); + + if (requestedIdentifiers.length === 0) { + throw new KeeperSdkError( + "No teams to delete.", + ResultCodes.NO_TEAMS_TO_DELETE, + ); + } + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(DELETE_TEAM_INCLUDES); + const allTeams = response.teams || []; + const resolvedTeams = resolveExistingTeams(allTeams, requestedIdentifiers); + + const items: DeleteTeamItemResult[] = []; + for (const team of resolvedTeams) { + try { + await sendTeamDelete(auth, team.team_uid); + items.push({ + teamUid: team.team_uid, + teamName: team.name || "", + nodeId: team.node_id, + status: DeleteTeamStatus.Deleted, + }); + } catch (err) { + items.push({ + teamUid: team.team_uid, + teamName: team.name || "", + nodeId: team.node_id, + status: DeleteTeamStatus.Failed, + message: extractErrorMessage(err), + }); } + } - return finalizeResult(items) + return finalizeResult(items); } async function sendTeamDelete(auth: Auth, teamUid: string): Promise { - const command: RestCommand = { - baseRequest: { command: TEAM_DELETE_COMMAND }, - request: { team_uid: teamUid }, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `team_delete failed for team_uid=${teamUid}`, - response.result_code || ResultCodes.TEAM_DELETE_FAILED - ) - } + const command: RestCommand = { + baseRequest: { command: TEAM_DELETE_COMMAND }, + request: { team_uid: teamUid }, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `team_delete failed for team_uid=${teamUid}`, + response.result_code || ResultCodes.TEAM_DELETE_FAILED, + ); + } } function finalizeResult(items: DeleteTeamItemResult[]): DeleteTeamResult { - const deleted = items.filter((item) => item.status === DeleteTeamStatus.Deleted).length - const failed = items.filter((item) => item.status === DeleteTeamStatus.Failed).length - return { - success: failed === 0 && deleted > 0, - items, - deleted, - failed, - } + const deleted = items.filter( + (item) => item.status === DeleteTeamStatus.Deleted, + ).length; + const failed = items.filter( + (item) => item.status === DeleteTeamStatus.Failed, + ).length; + return { + success: failed === 0 && deleted > 0, + items, + deleted, + failed, + }; } export type FormattedDeleteTeamRow = { - status: string - teamName: string - teamUid: string - nodeId: number - detail: string -} + status: string; + teamName: string; + teamUid: string; + nodeId: number; + detail: string; +}; export type FormattedDeleteTeamTable = { - headers: string[] - rows: string[][] - summary: string -} - -export function formatDeleteTeamResult(result: DeleteTeamResult): FormattedDeleteTeamTable { - const rows = result.items.map((item, index) => [ - String(index + 1), - item.status, - item.teamName, - item.teamUid, - String(item.nodeId), - item.message || '', - ]) - - return { - headers: [...TEAM_TABLE_HEADERS], - rows, - summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, - } + headers: string[]; + rows: string[][]; + summary: string; +}; + +export function formatDeleteTeamResult( + result: DeleteTeamResult, +): FormattedDeleteTeamTable { + const rows = result.items.map((item, index) => [ + String(index + 1), + item.status, + item.teamName, + item.teamUid, + String(item.nodeId), + item.message || "", + ]); + + return { + headers: [...TEAM_TABLE_HEADERS], + rows, + summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, + }; } -export function renderDeleteTeamAsciiTable(table: FormattedDeleteTeamTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell + ' '.repeat(Math.max(0, widths[columnIndex] - cell.length)) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [] - lines.push(formatRow(headers)) - lines.push(formatRow(widths.map((width) => '-'.repeat(width)))) - for (const row of rows) lines.push(formatRow(row)) - lines.push(table.summary) - return lines.join('\n') +export function renderDeleteTeamAsciiTable( + table: FormattedDeleteTeamTable, +): string { + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell + " ".repeat(Math.max(0, widths[columnIndex] - cell.length)); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = []; + lines.push(formatRow(headers)); + lines.push(formatRow(widths.map((width) => "-".repeat(width)))); + for (const row of rows) lines.push(formatRow(row)); + lines.push(table.summary); + return lines.join("\n"); } diff --git a/KeeperSdk/src/teams/enterpriseData.ts b/KeeperSdk/src/teams/enterpriseData.ts index d9336218..d73bd268 100644 --- a/KeeperSdk/src/teams/enterpriseData.ts +++ b/KeeperSdk/src/teams/enterpriseData.ts @@ -1,661 +1,795 @@ import { - decryptObjectFromStorage, - Enterprise, - getEnterpriseDataForUserMessage, - getEnterpriseDataKeysMessage, - normal64Bytes, - platform, - webSafe64FromBytes, - type Auth, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, isNumber, logger } from '../utils' - -const DEFAULT_NODE_PATH_SEPARATOR = '\\' -const MAX_CONTINUATIONS = 50 - + decryptObjectFromStorage, + Enterprise, + getEnterpriseDataForUserMessage, + getEnterpriseDataKeysMessage, + normal64Bytes, + platform, + webSafe64FromBytes, + type Auth, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, isNumber, logger } from "../utils"; + +const DEFAULT_NODE_PATH_SEPARATOR = "\\"; +const MAX_CONTINUATIONS = 50; export enum EnterpriseDataInclude { - Nodes = 'nodes', - Users = 'users', - Roles = 'roles', - RoleUsers = 'role_users', - RoleTeams = 'role_teams', - RolePrivileges = 'role_privileges', - ManagedNodes = 'managed_nodes', - RoleEnforcements = 'role_enforcements', - Teams = 'teams', - TeamUsers = 'team_users', - QueuedTeams = 'queued_teams', - QueuedTeamUsers = 'queued_team_users', - UserAliases = 'user_aliases', + Nodes = "nodes", + Users = "users", + Roles = "roles", + RoleUsers = "role_users", + RoleTeams = "role_teams", + RolePrivileges = "role_privileges", + ManagedNodes = "managed_nodes", + RoleEnforcements = "role_enforcements", + Teams = "teams", + TeamUsers = "team_users", + QueuedTeams = "queued_teams", + QueuedTeamUsers = "queued_team_users", + UserAliases = "user_aliases", } -const INCLUDE_TO_ENTITY: Record = { - [EnterpriseDataInclude.Nodes]: Enterprise.EnterpriseDataEntity.NODES, - [EnterpriseDataInclude.Users]: Enterprise.EnterpriseDataEntity.USERS, - [EnterpriseDataInclude.Roles]: Enterprise.EnterpriseDataEntity.ROLES, - [EnterpriseDataInclude.RoleUsers]: Enterprise.EnterpriseDataEntity.ROLE_USERS, - [EnterpriseDataInclude.RoleTeams]: Enterprise.EnterpriseDataEntity.ROLE_TEAMS, - [EnterpriseDataInclude.RolePrivileges]: Enterprise.EnterpriseDataEntity.ROLE_PRIVILEGES, - [EnterpriseDataInclude.ManagedNodes]: Enterprise.EnterpriseDataEntity.MANAGED_NODES, - [EnterpriseDataInclude.RoleEnforcements]: Enterprise.EnterpriseDataEntity.ROLE_ENFORCEMENTS, - [EnterpriseDataInclude.Teams]: Enterprise.EnterpriseDataEntity.TEAMS, - [EnterpriseDataInclude.TeamUsers]: Enterprise.EnterpriseDataEntity.TEAM_USERS, - [EnterpriseDataInclude.QueuedTeams]: Enterprise.EnterpriseDataEntity.QUEUED_TEAMS, - [EnterpriseDataInclude.QueuedTeamUsers]: Enterprise.EnterpriseDataEntity.QUEUED_TEAM_USERS, - [EnterpriseDataInclude.UserAliases]: Enterprise.EnterpriseDataEntity.USER_ALIASES, -} +const INCLUDE_TO_ENTITY: Record< + EnterpriseDataInclude, + Enterprise.EnterpriseDataEntity +> = { + [EnterpriseDataInclude.Nodes]: Enterprise.EnterpriseDataEntity.NODES, + [EnterpriseDataInclude.Users]: Enterprise.EnterpriseDataEntity.USERS, + [EnterpriseDataInclude.Roles]: Enterprise.EnterpriseDataEntity.ROLES, + [EnterpriseDataInclude.RoleUsers]: Enterprise.EnterpriseDataEntity.ROLE_USERS, + [EnterpriseDataInclude.RoleTeams]: Enterprise.EnterpriseDataEntity.ROLE_TEAMS, + [EnterpriseDataInclude.RolePrivileges]: + Enterprise.EnterpriseDataEntity.ROLE_PRIVILEGES, + [EnterpriseDataInclude.ManagedNodes]: + Enterprise.EnterpriseDataEntity.MANAGED_NODES, + [EnterpriseDataInclude.RoleEnforcements]: + Enterprise.EnterpriseDataEntity.ROLE_ENFORCEMENTS, + [EnterpriseDataInclude.Teams]: Enterprise.EnterpriseDataEntity.TEAMS, + [EnterpriseDataInclude.TeamUsers]: Enterprise.EnterpriseDataEntity.TEAM_USERS, + [EnterpriseDataInclude.QueuedTeams]: + Enterprise.EnterpriseDataEntity.QUEUED_TEAMS, + [EnterpriseDataInclude.QueuedTeamUsers]: + Enterprise.EnterpriseDataEntity.QUEUED_TEAM_USERS, + [EnterpriseDataInclude.UserAliases]: + Enterprise.EnterpriseDataEntity.USER_ALIASES, +}; export type EnterpriseNode = { - node_id: number - parent_id?: number - encrypted_data?: string - displayName?: string -} + node_id: number; + parent_id?: number; + encrypted_data?: string; + displayName?: string; +}; export type EnterpriseUser = { - enterprise_user_id: number - username: string - status?: string - node_id?: number - encrypted_data?: string - full_name?: string - job_title?: string - lock?: number - account_share_expiration?: number - tfa_enabled?: boolean -} + enterprise_user_id: number; + username: string; + status?: string; + node_id?: number; + encrypted_data?: string; + full_name?: string; + job_title?: string; + lock?: number; + account_share_expiration?: number; + tfa_enabled?: boolean; +}; export type EnterpriseRole = { - role_id: number - node_id?: number - encrypted_data?: string - displayName?: string - visible_below?: boolean - new_user_inherit?: boolean -} + role_id: number; + node_id?: number; + encrypted_data?: string; + displayName?: string; + visible_below?: boolean; + new_user_inherit?: boolean; +}; export type EnterpriseRolePrivilegeLink = { - role_id: number - managed_node_id: number - privilege_type?: string -} + role_id: number; + managed_node_id: number; + privilege_type?: string; +}; export type EnterpriseRoleManagedNodeLink = { - role_id: number - managed_node_id: number - cascade_node_management: boolean -} + role_id: number; + managed_node_id: number; + cascade_node_management: boolean; +}; export type EnterpriseRoleEnforcementLink = { - role_id: number - enforcement_type: string - value?: string -} + role_id: number; + enforcement_type: string; + value?: string; +}; export type EnterpriseTeamRecord = { - team_uid: string - name: string - node_id: number - restrict_view?: boolean - restrict_edit?: boolean - restrict_share?: boolean - restrict_sharing?: boolean - encrypted_data?: string - encrypted_team_key?: string -} + team_uid: string; + name: string; + node_id: number; + restrict_view?: boolean; + restrict_edit?: boolean; + restrict_share?: boolean; + restrict_sharing?: boolean; + encrypted_data?: string; + encrypted_team_key?: string; +}; export type EnterpriseTeamUserLink = { - team_uid: string - enterprise_user_id: number - user_type?: string -} + team_uid: string; + enterprise_user_id: number; + user_type?: string; +}; export type EnterpriseRoleUserLink = { - role_id: number - enterprise_user_id: number -} + role_id: number; + enterprise_user_id: number; +}; export type EnterpriseRoleTeamLink = { - role_id: number - team_uid: string -} + role_id: number; + team_uid: string; +}; export type EnterpriseQueuedTeamRecord = { - team_uid: string - name: string - node_id: number - encrypted_data?: string -} + team_uid: string; + name: string; + node_id: number; + encrypted_data?: string; +}; export type EnterpriseQueuedTeamUserLink = { - team_uid: string - enterprise_user_id: number -} + team_uid: string; + enterprise_user_id: number; +}; export type EnterpriseUserAliasLink = { - enterprise_user_id: number - username: string -} + enterprise_user_id: number; + username: string; +}; export type GetEnterpriseDataResponse = { - enterprise_name?: string - nodes?: EnterpriseNode[] - users?: EnterpriseUser[] - roles?: EnterpriseRole[] - teams?: EnterpriseTeamRecord[] - team_users?: EnterpriseTeamUserLink[] - role_users?: EnterpriseRoleUserLink[] - role_teams?: EnterpriseRoleTeamLink[] - role_privileges?: EnterpriseRolePrivilegeLink[] - managed_nodes?: EnterpriseRoleManagedNodeLink[] - role_enforcements?: EnterpriseRoleEnforcementLink[] - queued_teams?: EnterpriseQueuedTeamRecord[] - queued_team_users?: EnterpriseQueuedTeamUserLink[] - user_aliases?: EnterpriseUserAliasLink[] -} + enterprise_name?: string; + nodes?: EnterpriseNode[]; + users?: EnterpriseUser[]; + roles?: EnterpriseRole[]; + teams?: EnterpriseTeamRecord[]; + team_users?: EnterpriseTeamUserLink[]; + role_users?: EnterpriseRoleUserLink[]; + role_teams?: EnterpriseRoleTeamLink[]; + role_privileges?: EnterpriseRolePrivilegeLink[]; + managed_nodes?: EnterpriseRoleManagedNodeLink[]; + role_enforcements?: EnterpriseRoleEnforcementLink[]; + queued_teams?: EnterpriseQueuedTeamRecord[]; + queued_team_users?: EnterpriseQueuedTeamUserLink[]; + user_aliases?: EnterpriseUserAliasLink[]; +}; export type NodePathOptions = { - omitRoot?: boolean - separator?: string -} + omitRoot?: boolean; + separator?: string; +}; -export type DecryptedNodeNames = Map -export type DecryptedRoleNames = Map +export type DecryptedNodeNames = Map; +export type DecryptedRoleNames = Map; export type EnterpriseDisplayNames = { - nodes: DecryptedNodeNames - roles: DecryptedRoleNames -} + nodes: DecryptedNodeNames; + roles: DecryptedRoleNames; +}; -type LongLike = number | { toNumber: () => number; toString: () => string } | undefined | null +type LongLike = + | number + | { toNumber: () => number; toString: () => string } + | undefined + | null; export interface EnterpriseDataManagerApi { - getData(includes: EnterpriseDataInclude[]): Promise - getDisplayNames(): Promise - getTreeKey(): Promise - decryptNodeNames(nodes: EnterpriseNode[]): Promise - clearCache(): void + getData( + includes: EnterpriseDataInclude[], + ): Promise; + getDisplayNames(): Promise; + getTreeKey(): Promise; + decryptNodeNames(nodes: EnterpriseNode[]): Promise; + clearCache(): void; } export class EnterpriseDataManager implements EnterpriseDataManagerApi { - private readonly auth: Auth - private displayNamesPromise: Promise | null = null - private treeKeyPromise: Promise | null = null - private readonly dataCache = new Map>() - - constructor(auth: Auth) { - this.auth = auth - } - - public async getData(includes: EnterpriseDataInclude[]): Promise { - const key = EnterpriseDataManager.cacheKeyForIncludes(includes) - let promise = this.dataCache.get(key) - if (!promise) { - promise = this.fetchEnterpriseData(includes) - this.dataCache.set(key, promise) - } - return promise - } - - public async getDisplayNames(): Promise { - if (!this.displayNamesPromise) { - this.displayNamesPromise = this.fetchDisplayNames() - } - return this.displayNamesPromise - } - - public async decryptNodeNames(nodes: EnterpriseNode[]): Promise { - if (!nodes || nodes.length === 0) return 0 - - const needsDecrypt = nodes.some( - (node) => !!node.encrypted_data && !(node.displayName || '').trim() - ) - if (!needsDecrypt) return 0 - - const displayNames = await this.getDisplayNames() - if (displayNames.nodes.size > 0) { - for (const node of nodes) { - if ((node.displayName || '').trim()) continue - const cached = displayNames.nodes.get(node.node_id) - if (cached) node.displayName = cached - } - } - - const treeKey = await this.getTreeKey() - if (!treeKey) { - logger.warn( - 'Enterprise node names unavailable: could not decrypt enterprise tree key. ' + - 'Use numeric node IDs (visible in error listings).' - ) - return 0 - } - - let decryptedCount = 0 - for (const node of nodes) { - if ((node.displayName || '').trim()) continue - if (!node.encrypted_data) continue - const display = await EnterpriseDataManager.decryptDisplayName(node.encrypted_data, treeKey) - if (display) { - node.displayName = display - decryptedCount++ - } - } - if (decryptedCount > 0) { - logger.debug(`Decrypted ${decryptedCount} enterprise node name(s) using tree key.`) - } - return decryptedCount - } - - public clearCache(): void { - this.dataCache.clear() - this.displayNamesPromise = null - this.treeKeyPromise = null - } - - public static getNodePath( - nodes: EnterpriseNode[], - nodeId: number, - options: NodePathOptions = {} - ): string { - const { omitRoot = true, separator = DEFAULT_NODE_PATH_SEPARATOR } = options - const byId = new Map() - for (const node of nodes) byId.set(node.node_id, node) - - const visited = new Set() - const segments: string[] = [] - let currentId: number | undefined = nodeId - while (isNumber(currentId) && currentId > 0 && !visited.has(currentId)) { - visited.add(currentId) - const node = byId.get(currentId) - if (!node) break - const parentId = node.parent_id || 0 - const isRoot = parentId === 0 - if (!isRoot || !omitRoot) { - const segmentName = (node.displayName || '').trim() - if (segmentName) segments.push(segmentName) - } - currentId = parentId - } - segments.reverse() - return segments.join(separator) - } - - private static cacheKeyForIncludes(includes: EnterpriseDataInclude[]): string { - return [...includes].sort().join(',') - } - - private async fetchEnterpriseData( - includes: EnterpriseDataInclude[] - ): Promise { - const interesting = new Set( - includes.map((key) => INCLUDE_TO_ENTITY[key]) - ) - const aggregate: GetEnterpriseDataResponse = {} - let continuationToken: Uint8Array | undefined - - for (let iteration = 0; iteration < MAX_CONTINUATIONS; iteration += 1) { - const request: Enterprise.IEnterpriseDataRequest = {} - if (continuationToken) request.continuationToken = continuationToken - - const response = await this.auth.executeRest(getEnterpriseDataForUserMessage(request)) - - if (response.generalData?.enterpriseName) { - aggregate.enterprise_name = response.generalData.enterpriseName - } - - for (const chunk of response.data || []) { - const entity = chunk.entity ?? Enterprise.EnterpriseDataEntity.UNKNOWN - if (!interesting.has(entity)) continue - EnterpriseDataManager.applyChunk(chunk, aggregate) - } - - if (!response.hasMore) break - if (!response.continuationToken || response.continuationToken.length === 0) break - continuationToken = response.continuationToken - } - - return aggregate + private readonly auth: Auth; + private displayNamesPromise: Promise | null = null; + private treeKeyPromise: Promise | null = null; + private readonly dataCache = new Map< + string, + Promise + >(); + + constructor(auth: Auth) { + this.auth = auth; + } + + public async getData( + includes: EnterpriseDataInclude[], + ): Promise { + const key = EnterpriseDataManager.cacheKeyForIncludes(includes); + let promise = this.dataCache.get(key); + if (!promise) { + promise = this.fetchEnterpriseData(includes); + this.dataCache.set(key, promise); } + return promise; + } - private async fetchDisplayNames(): Promise { - const empty: EnterpriseDisplayNames = { nodes: new Map(), roles: new Map() } - - const treeKey = await this.getTreeKey() - if (!treeKey) return empty - - const data = await this.getData([EnterpriseDataInclude.Nodes, EnterpriseDataInclude.Roles]) - - const nodes: DecryptedNodeNames = new Map() - for (const node of data.nodes || []) { - if (!isNumber(node.node_id)) continue - if (!node.encrypted_data) continue - const display = await EnterpriseDataManager.decryptDisplayName(node.encrypted_data, treeKey) - if (display) nodes.set(node.node_id, display) - } - - const roles: DecryptedRoleNames = new Map() - for (const role of data.roles || []) { - if (!isNumber(role.role_id)) continue - if (!role.encrypted_data) continue - const display = await EnterpriseDataManager.decryptDisplayName(role.encrypted_data, treeKey) - if (display) roles.set(role.role_id, display) - } - - return { nodes, roles } + public async getDisplayNames(): Promise { + if (!this.displayNamesPromise) { + this.displayNamesPromise = this.fetchDisplayNames(); } - - public async getTreeKey(): Promise { - if (!this.treeKeyPromise) { - this.treeKeyPromise = this.fetchAndDecryptTreeKey() - } - return this.treeKeyPromise + return this.displayNamesPromise; + } + + public async decryptNodeNames(nodes: EnterpriseNode[]): Promise { + if (!nodes || nodes.length === 0) return 0; + + const needsDecrypt = nodes.some( + (node) => !!node.encrypted_data && !(node.displayName || "").trim(), + ); + if (!needsDecrypt) return 0; + + const displayNames = await this.getDisplayNames(); + if (displayNames.nodes.size > 0) { + for (const node of nodes) { + if ((node.displayName || "").trim()) continue; + const cached = displayNames.nodes.get(node.node_id); + if (cached) node.displayName = cached; + } } - private async fetchAndDecryptTreeKey(): Promise { - let response: Enterprise.IGetEnterpriseDataKeysResponse - try { - response = await this.auth.executeRest(getEnterpriseDataKeysMessage()) - } catch (err) { - logger.debug(`enterprise/get_enterprise_data_keys failed: ${extractErrorMessage(err)}`) - return null - } - - const treeKey = response.treeKey - if (!treeKey || !treeKey.treeKey) { - logger.debug( - `enterprise/get_enterprise_data_keys: no tree key returned (user is likely not an enterprise admin)` - ) - return null - } - - return this.decryptTreeKey(treeKey) + const treeKey = await this.getTreeKey(); + if (!treeKey) { + logger.warn( + "Enterprise node names unavailable: could not decrypt enterprise tree key. " + + "Use numeric node IDs (visible in error listings).", + ); + return 0; } - private async decryptTreeKey(treeKey: Enterprise.ITreeKey): Promise { - if (!treeKey.treeKey) return null - const encrypted = normal64Bytes(treeKey.treeKey) - const keyType = (treeKey.keyTypeId ?? 0) as Enterprise.BackupKeyType - - try { - switch (keyType) { - case Enterprise.BackupKeyType.ENCRYPTED_BY_DATA_KEY: { - if (!this.auth.dataKey) return null - return await platform.aesCbcDecrypt(encrypted, this.auth.dataKey, true) - } - case Enterprise.BackupKeyType.ENCRYPTED_BY_DATA_KEY_GCM: { - if (!this.auth.dataKey) return null - return await platform.aesGcmDecrypt(encrypted, this.auth.dataKey) - } - case Enterprise.BackupKeyType.ENCRYPTED_BY_PUBLIC_KEY: { - if (!this.auth.privateKey) return null - return platform.privateDecrypt(encrypted, this.auth.privateKey) - } - case Enterprise.BackupKeyType.ENCRYPTED_BY_PUBLIC_KEY_ECC: { - if (!this.auth.eccPrivateKey) return null - return await platform.privateDecryptEC(encrypted, this.auth.eccPrivateKey) - } - default: - logger.debug(`Unsupported tree-key keyTypeId=${keyType}`) - return null - } - } catch (err) { - logger.debug( - `Tree-key decryption failed (keyTypeId=${keyType}): ${extractErrorMessage(err)}` - ) - return null - } + let decryptedCount = 0; + for (const node of nodes) { + if ((node.displayName || "").trim()) continue; + if (!node.encrypted_data) continue; + const display = await EnterpriseDataManager.decryptDisplayName( + node.encrypted_data, + treeKey, + ); + if (display) { + node.displayName = display; + decryptedCount++; + } } - - private static async decryptDisplayName(encrypted: string, treeKey: Uint8Array): Promise { - try { - const decrypted = await decryptObjectFromStorage<{ displayname?: string }>(encrypted, treeKey) - return (decrypted?.displayname || '').trim() - } catch { - return '' - } + if (decryptedCount > 0) { + logger.debug( + `Decrypted ${decryptedCount} enterprise node name(s) using tree key.`, + ); } - - private static toNumber(value: LongLike): number { - if (value == null) return 0 - if (isNumber(value)) return value - if (typeof value.toNumber === 'function') return value.toNumber() - const parsed = Number(value.toString()) - return Number.isFinite(parsed) ? parsed : 0 + return decryptedCount; + } + + public clearCache(): void { + this.dataCache.clear(); + this.displayNamesPromise = null; + this.treeKeyPromise = null; + } + + public static getNodePath( + nodes: EnterpriseNode[], + nodeId: number, + options: NodePathOptions = {}, + ): string { + const { omitRoot = true, separator = DEFAULT_NODE_PATH_SEPARATOR } = + options; + const byId = new Map(); + for (const node of nodes) byId.set(node.node_id, node); + + const visited = new Set(); + const segments: string[] = []; + let currentId: number | undefined = nodeId; + while (isNumber(currentId) && currentId > 0 && !visited.has(currentId)) { + visited.add(currentId); + const node = byId.get(currentId); + if (!node) break; + const parentId = node.parent_id || 0; + const isRoot = parentId === 0; + if (!isRoot || !omitRoot) { + const segmentName = (node.displayName || "").trim(); + if (segmentName) segments.push(segmentName); + } + currentId = parentId; } - - private static toUid(bytes: Uint8Array | null | undefined): string { - return bytes && bytes.length > 0 ? webSafe64FromBytes(bytes) : '' + segments.reverse(); + return segments.join(separator); + } + + private static cacheKeyForIncludes( + includes: EnterpriseDataInclude[], + ): string { + return [...includes].sort().join(","); + } + + private async fetchEnterpriseData( + includes: EnterpriseDataInclude[], + ): Promise { + const interesting = new Set( + includes.map((key) => INCLUDE_TO_ENTITY[key]), + ); + const aggregate: GetEnterpriseDataResponse = {}; + let continuationToken: Uint8Array | undefined; + + for (let iteration = 0; iteration < MAX_CONTINUATIONS; iteration += 1) { + const request: Enterprise.IEnterpriseDataRequest = {}; + if (continuationToken) request.continuationToken = continuationToken; + + const response = await this.auth.executeRest( + getEnterpriseDataForUserMessage(request), + ); + + if (response.generalData?.enterpriseName) { + aggregate.enterprise_name = response.generalData.enterpriseName; + } + + for (const chunk of response.data || []) { + const entity = chunk.entity ?? Enterprise.EnterpriseDataEntity.UNKNOWN; + if (!interesting.has(entity)) continue; + EnterpriseDataManager.applyChunk(chunk, aggregate); + } + + if (!response.hasMore) break; + if ( + !response.continuationToken || + response.continuationToken.length === 0 + ) + break; + continuationToken = response.continuationToken; } - private static decodeChunk( - chunkData: Uint8Array[] | null | undefined, - decoder: (bytes: Uint8Array) => T - ): T[] { - if (!chunkData || chunkData.length === 0) return [] - const results: T[] = [] - for (const bytes of chunkData) { - try { - results.push(decoder(bytes)) - } catch { - } - } - return results + return aggregate; + } + + private async fetchDisplayNames(): Promise { + const empty: EnterpriseDisplayNames = { + nodes: new Map(), + roles: new Map(), + }; + + const treeKey = await this.getTreeKey(); + if (!treeKey) return empty; + + const data = await this.getData([ + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Roles, + ]); + + const nodes: DecryptedNodeNames = new Map(); + for (const node of data.nodes || []) { + if (!isNumber(node.node_id)) continue; + if (!node.encrypted_data) continue; + const display = await EnterpriseDataManager.decryptDisplayName( + node.encrypted_data, + treeKey, + ); + if (display) nodes.set(node.node_id, display); } - private static decodeNodeChunk(bytes: Uint8Array): EnterpriseNode { - const message = Enterprise.Node.decode(bytes) - const node: EnterpriseNode = { node_id: EnterpriseDataManager.toNumber(message.nodeId) } - if (message.parentId != null) node.parent_id = EnterpriseDataManager.toNumber(message.parentId) - if (message.encryptedData) node.encrypted_data = message.encryptedData - return node + const roles: DecryptedRoleNames = new Map(); + for (const role of data.roles || []) { + if (!isNumber(role.role_id)) continue; + if (!role.encrypted_data) continue; + const display = await EnterpriseDataManager.decryptDisplayName( + role.encrypted_data, + treeKey, + ); + if (display) roles.set(role.role_id, display); } - private static decodeUserChunk(bytes: Uint8Array): EnterpriseUser { - const message = Enterprise.User.decode(bytes) - const user: EnterpriseUser = { - enterprise_user_id: EnterpriseDataManager.toNumber(message.enterpriseUserId), - username: message.username || '', - } - if (message.status) user.status = message.status - if (message.nodeId != null) user.node_id = EnterpriseDataManager.toNumber(message.nodeId) - if (message.encryptedData) user.encrypted_data = message.encryptedData - if (message.fullName) user.full_name = message.fullName - if (message.jobTitle) user.job_title = message.jobTitle - if (message.lock != null) user.lock = message.lock - if (message.accountShareExpiration != null) { - user.account_share_expiration = EnterpriseDataManager.toNumber(message.accountShareExpiration) - } - if (message.tfaEnabled != null) user.tfa_enabled = message.tfaEnabled - return user - } + return { nodes, roles }; + } - private static decodeRoleChunk(bytes: Uint8Array): EnterpriseRole { - const message = Enterprise.Role.decode(bytes) - const out: EnterpriseRole = { role_id: EnterpriseDataManager.toNumber(message.roleId) } - if (message.nodeId != null) out.node_id = EnterpriseDataManager.toNumber(message.nodeId) - if (message.encryptedData) out.encrypted_data = message.encryptedData - if (message.visibleBelow != null) out.visible_below = message.visibleBelow - if (message.newUserInherit != null) out.new_user_inherit = message.newUserInherit - return out + public async getTreeKey(): Promise { + if (!this.treeKeyPromise) { + this.treeKeyPromise = this.fetchAndDecryptTreeKey(); } - - private static decodeRolePrivilegeChunk(bytes: Uint8Array): EnterpriseRolePrivilegeLink { - const message = Enterprise.RolePrivilege.decode(bytes) - const out: EnterpriseRolePrivilegeLink = { - role_id: EnterpriseDataManager.toNumber(message.roleId), - managed_node_id: EnterpriseDataManager.toNumber(message.managedNodeId), - } - if (message.privilegeType) out.privilege_type = message.privilegeType - return out + return this.treeKeyPromise; + } + + private async fetchAndDecryptTreeKey(): Promise { + let response: Enterprise.IGetEnterpriseDataKeysResponse; + try { + response = await this.auth.executeRest(getEnterpriseDataKeysMessage()); + } catch (err) { + logger.debug( + `enterprise/get_enterprise_data_keys failed: ${extractErrorMessage(err)}`, + ); + return null; } - private static decodeManagedNodeChunk(bytes: Uint8Array): EnterpriseRoleManagedNodeLink { - const message = Enterprise.ManagedNode.decode(bytes) - return { - role_id: EnterpriseDataManager.toNumber(message.roleId), - managed_node_id: EnterpriseDataManager.toNumber(message.managedNodeId), - cascade_node_management: message.cascadeNodeManagement === true, - } + const treeKey = response.treeKey; + if (!treeKey || !treeKey.treeKey) { + logger.debug( + `enterprise/get_enterprise_data_keys: no tree key returned (user is likely not an enterprise admin)`, + ); + return null; } - private static decodeRoleEnforcementChunk(bytes: Uint8Array): EnterpriseRoleEnforcementLink { - const message = Enterprise.RoleEnforcement.decode(bytes) - const out: EnterpriseRoleEnforcementLink = { - role_id: EnterpriseDataManager.toNumber(message.roleId), - enforcement_type: message.enforcementType || '', + return this.decryptTreeKey(treeKey); + } + + private async decryptTreeKey( + treeKey: Enterprise.ITreeKey, + ): Promise { + if (!treeKey.treeKey) return null; + const encrypted = normal64Bytes(treeKey.treeKey); + const keyType = (treeKey.keyTypeId ?? 0) as Enterprise.BackupKeyType; + + try { + switch (keyType) { + case Enterprise.BackupKeyType.ENCRYPTED_BY_DATA_KEY: { + if (!this.auth.dataKey) return null; + return await platform.aesCbcDecrypt( + encrypted, + this.auth.dataKey, + true, + ); } - if (message.value) out.value = message.value - return out - } - - private static decodeTeamChunk(bytes: Uint8Array): EnterpriseTeamRecord { - const message = Enterprise.Team.decode(bytes) - const team: EnterpriseTeamRecord = { - team_uid: EnterpriseDataManager.toUid(message.teamUid), - name: message.name || '', - node_id: EnterpriseDataManager.toNumber(message.nodeId), - restrict_view: message.restrictView === true, - restrict_edit: message.restrictEdit === true, - restrict_share: message.restrictShare === true, + case Enterprise.BackupKeyType.ENCRYPTED_BY_DATA_KEY_GCM: { + if (!this.auth.dataKey) return null; + return await platform.aesGcmDecrypt(encrypted, this.auth.dataKey); } - if (message.encryptedData) team.encrypted_data = message.encryptedData - if (message.encryptedTeamKey) team.encrypted_team_key = message.encryptedTeamKey - return team - } - - private static decodeTeamUserChunk(bytes: Uint8Array): EnterpriseTeamUserLink { - const message = Enterprise.TeamUser.decode(bytes) - const teamUser: EnterpriseTeamUserLink = { - team_uid: EnterpriseDataManager.toUid(message.teamUid), - enterprise_user_id: EnterpriseDataManager.toNumber(message.enterpriseUserId), + case Enterprise.BackupKeyType.ENCRYPTED_BY_PUBLIC_KEY: { + if (!this.auth.privateKey) return null; + return platform.privateDecrypt(encrypted, this.auth.privateKey); } - if (message.userType) teamUser.user_type = message.userType - return teamUser - } - - private static decodeRoleUserChunk(bytes: Uint8Array): EnterpriseRoleUserLink { - const message = Enterprise.RoleUser.decode(bytes) - return { - role_id: EnterpriseDataManager.toNumber(message.roleId), - enterprise_user_id: EnterpriseDataManager.toNumber(message.enterpriseUserId), + case Enterprise.BackupKeyType.ENCRYPTED_BY_PUBLIC_KEY_ECC: { + if (!this.auth.eccPrivateKey) return null; + return await platform.privateDecryptEC( + encrypted, + this.auth.eccPrivateKey, + ); } + default: + logger.debug(`Unsupported tree-key keyTypeId=${keyType}`); + return null; + } + } catch (err) { + logger.debug( + `Tree-key decryption failed (keyTypeId=${keyType}): ${extractErrorMessage(err)}`, + ); + return null; } - - private static decodeRoleTeamChunk(bytes: Uint8Array): EnterpriseRoleTeamLink { - const message = Enterprise.RoleTeam.decode(bytes) - return { - role_id: EnterpriseDataManager.toNumber(message.roleId), - team_uid: EnterpriseDataManager.toUid(message.teamUid), - } + } + + private static async decryptDisplayName( + encrypted: string, + treeKey: Uint8Array, + ): Promise { + try { + const decrypted = await decryptObjectFromStorage<{ + displayname?: string; + }>(encrypted, treeKey); + return (decrypted?.displayname || "").trim(); + } catch { + return ""; } - - private static decodeQueuedTeamChunk(bytes: Uint8Array): EnterpriseQueuedTeamRecord { - const message = Enterprise.QueuedTeam.decode(bytes) - const queuedTeam: EnterpriseQueuedTeamRecord = { - team_uid: EnterpriseDataManager.toUid(message.teamUid), - name: message.name || '', - node_id: EnterpriseDataManager.toNumber(message.nodeId), - } - if (message.encryptedData) queuedTeam.encrypted_data = message.encryptedData - return queuedTeam + } + + private static toNumber(value: LongLike): number { + if (value == null) return 0; + if (isNumber(value)) return value; + if (typeof value.toNumber === "function") return value.toNumber(); + const parsed = Number(value.toString()); + return Number.isFinite(parsed) ? parsed : 0; + } + + private static toUid(bytes: Uint8Array | null | undefined): string { + return bytes && bytes.length > 0 ? webSafe64FromBytes(bytes) : ""; + } + + private static decodeChunk( + chunkData: Uint8Array[] | null | undefined, + decoder: (bytes: Uint8Array) => T, + ): T[] { + if (!chunkData || chunkData.length === 0) return []; + const results: T[] = []; + for (const bytes of chunkData) { + try { + results.push(decoder(bytes)); + } catch {} } - - private static decodeUserAliasChunk(bytes: Uint8Array): EnterpriseUserAliasLink { - const message = Enterprise.UserAlias.decode(bytes) - return { - enterprise_user_id: EnterpriseDataManager.toNumber(message.enterpriseUserId), - username: message.username || '', - } + return results; + } + + private static decodeNodeChunk(bytes: Uint8Array): EnterpriseNode { + const message = Enterprise.Node.decode(bytes); + const node: EnterpriseNode = { + node_id: EnterpriseDataManager.toNumber(message.nodeId), + }; + if (message.parentId != null) + node.parent_id = EnterpriseDataManager.toNumber(message.parentId); + if (message.encryptedData) node.encrypted_data = message.encryptedData; + return node; + } + + private static decodeUserChunk(bytes: Uint8Array): EnterpriseUser { + const message = Enterprise.User.decode(bytes); + const user: EnterpriseUser = { + enterprise_user_id: EnterpriseDataManager.toNumber( + message.enterpriseUserId, + ), + username: message.username || "", + }; + if (message.status) user.status = message.status; + if (message.nodeId != null) + user.node_id = EnterpriseDataManager.toNumber(message.nodeId); + if (message.encryptedData) user.encrypted_data = message.encryptedData; + if (message.fullName) user.full_name = message.fullName; + if (message.jobTitle) user.job_title = message.jobTitle; + if (message.lock != null) user.lock = message.lock; + if (message.accountShareExpiration != null) { + user.account_share_expiration = EnterpriseDataManager.toNumber( + message.accountShareExpiration, + ); } - - private static applyChunk( - chunk: Enterprise.IEnterpriseData, - target: GetEnterpriseDataResponse - ): void { - const entity = chunk.entity ?? Enterprise.EnterpriseDataEntity.UNKNOWN - const data = chunk.data || [] - - switch (entity) { - case Enterprise.EnterpriseDataEntity.NODES: - target.nodes = (target.nodes || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeNodeChunk) - ) - break - case Enterprise.EnterpriseDataEntity.USERS: - target.users = (target.users || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeUserChunk) - ) - break - case Enterprise.EnterpriseDataEntity.ROLES: - target.roles = (target.roles || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeRoleChunk) - ) - break - case Enterprise.EnterpriseDataEntity.TEAMS: - target.teams = (target.teams || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeTeamChunk) - ) - break - case Enterprise.EnterpriseDataEntity.TEAM_USERS: - target.team_users = (target.team_users || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeTeamUserChunk) - ) - break - case Enterprise.EnterpriseDataEntity.ROLE_USERS: - target.role_users = (target.role_users || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeRoleUserChunk) - ) - break - case Enterprise.EnterpriseDataEntity.ROLE_TEAMS: - target.role_teams = (target.role_teams || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeRoleTeamChunk) - ) - break - case Enterprise.EnterpriseDataEntity.QUEUED_TEAMS: - target.queued_teams = (target.queued_teams || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeQueuedTeamChunk) - ) - break - case Enterprise.EnterpriseDataEntity.QUEUED_TEAM_USERS: { - const flatLinks: EnterpriseQueuedTeamUserLink[] = [] - for (const bytes of data) { - try { - const message = Enterprise.QueuedTeamUser.decode(bytes) - const teamUid = EnterpriseDataManager.toUid(message.teamUid) - if (!teamUid) continue - for (const userId of message.users || []) { - flatLinks.push({ - team_uid: teamUid, - enterprise_user_id: EnterpriseDataManager.toNumber(userId), - }) - } - } catch {} - } - target.queued_team_users = (target.queued_team_users || []).concat(flatLinks) - break + if (message.tfaEnabled != null) user.tfa_enabled = message.tfaEnabled; + return user; + } + + private static decodeRoleChunk(bytes: Uint8Array): EnterpriseRole { + const message = Enterprise.Role.decode(bytes); + const out: EnterpriseRole = { + role_id: EnterpriseDataManager.toNumber(message.roleId), + }; + if (message.nodeId != null) + out.node_id = EnterpriseDataManager.toNumber(message.nodeId); + if (message.encryptedData) out.encrypted_data = message.encryptedData; + if (message.visibleBelow != null) out.visible_below = message.visibleBelow; + if (message.newUserInherit != null) + out.new_user_inherit = message.newUserInherit; + return out; + } + + private static decodeRolePrivilegeChunk( + bytes: Uint8Array, + ): EnterpriseRolePrivilegeLink { + const message = Enterprise.RolePrivilege.decode(bytes); + const out: EnterpriseRolePrivilegeLink = { + role_id: EnterpriseDataManager.toNumber(message.roleId), + managed_node_id: EnterpriseDataManager.toNumber(message.managedNodeId), + }; + if (message.privilegeType) out.privilege_type = message.privilegeType; + return out; + } + + private static decodeManagedNodeChunk( + bytes: Uint8Array, + ): EnterpriseRoleManagedNodeLink { + const message = Enterprise.ManagedNode.decode(bytes); + return { + role_id: EnterpriseDataManager.toNumber(message.roleId), + managed_node_id: EnterpriseDataManager.toNumber(message.managedNodeId), + cascade_node_management: message.cascadeNodeManagement === true, + }; + } + + private static decodeRoleEnforcementChunk( + bytes: Uint8Array, + ): EnterpriseRoleEnforcementLink { + const message = Enterprise.RoleEnforcement.decode(bytes); + const out: EnterpriseRoleEnforcementLink = { + role_id: EnterpriseDataManager.toNumber(message.roleId), + enforcement_type: message.enforcementType || "", + }; + if (message.value) out.value = message.value; + return out; + } + + private static decodeTeamChunk(bytes: Uint8Array): EnterpriseTeamRecord { + const message = Enterprise.Team.decode(bytes); + const team: EnterpriseTeamRecord = { + team_uid: EnterpriseDataManager.toUid(message.teamUid), + name: message.name || "", + node_id: EnterpriseDataManager.toNumber(message.nodeId), + restrict_view: message.restrictView === true, + restrict_edit: message.restrictEdit === true, + restrict_share: message.restrictShare === true, + }; + if (message.encryptedData) team.encrypted_data = message.encryptedData; + if (message.encryptedTeamKey) + team.encrypted_team_key = message.encryptedTeamKey; + return team; + } + + private static decodeTeamUserChunk( + bytes: Uint8Array, + ): EnterpriseTeamUserLink { + const message = Enterprise.TeamUser.decode(bytes); + const teamUser: EnterpriseTeamUserLink = { + team_uid: EnterpriseDataManager.toUid(message.teamUid), + enterprise_user_id: EnterpriseDataManager.toNumber( + message.enterpriseUserId, + ), + }; + if (message.userType) teamUser.user_type = message.userType; + return teamUser; + } + + private static decodeRoleUserChunk( + bytes: Uint8Array, + ): EnterpriseRoleUserLink { + const message = Enterprise.RoleUser.decode(bytes); + return { + role_id: EnterpriseDataManager.toNumber(message.roleId), + enterprise_user_id: EnterpriseDataManager.toNumber( + message.enterpriseUserId, + ), + }; + } + + private static decodeRoleTeamChunk( + bytes: Uint8Array, + ): EnterpriseRoleTeamLink { + const message = Enterprise.RoleTeam.decode(bytes); + return { + role_id: EnterpriseDataManager.toNumber(message.roleId), + team_uid: EnterpriseDataManager.toUid(message.teamUid), + }; + } + + private static decodeQueuedTeamChunk( + bytes: Uint8Array, + ): EnterpriseQueuedTeamRecord { + const message = Enterprise.QueuedTeam.decode(bytes); + const queuedTeam: EnterpriseQueuedTeamRecord = { + team_uid: EnterpriseDataManager.toUid(message.teamUid), + name: message.name || "", + node_id: EnterpriseDataManager.toNumber(message.nodeId), + }; + if (message.encryptedData) + queuedTeam.encrypted_data = message.encryptedData; + return queuedTeam; + } + + private static decodeUserAliasChunk( + bytes: Uint8Array, + ): EnterpriseUserAliasLink { + const message = Enterprise.UserAlias.decode(bytes); + return { + enterprise_user_id: EnterpriseDataManager.toNumber( + message.enterpriseUserId, + ), + username: message.username || "", + }; + } + + private static applyChunk( + chunk: Enterprise.IEnterpriseData, + target: GetEnterpriseDataResponse, + ): void { + const entity = chunk.entity ?? Enterprise.EnterpriseDataEntity.UNKNOWN; + const data = chunk.data || []; + + switch (entity) { + case Enterprise.EnterpriseDataEntity.NODES: + target.nodes = (target.nodes || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeNodeChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.USERS: + target.users = (target.users || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeUserChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.ROLES: + target.roles = (target.roles || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeRoleChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.TEAMS: + target.teams = (target.teams || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeTeamChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.TEAM_USERS: + target.team_users = (target.team_users || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeTeamUserChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.ROLE_USERS: + target.role_users = (target.role_users || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeRoleUserChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.ROLE_TEAMS: + target.role_teams = (target.role_teams || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeRoleTeamChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.QUEUED_TEAMS: + target.queued_teams = (target.queued_teams || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeQueuedTeamChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.QUEUED_TEAM_USERS: { + const flatLinks: EnterpriseQueuedTeamUserLink[] = []; + for (const bytes of data) { + try { + const message = Enterprise.QueuedTeamUser.decode(bytes); + const teamUid = EnterpriseDataManager.toUid(message.teamUid); + if (!teamUid) continue; + for (const userId of message.users || []) { + flatLinks.push({ + team_uid: teamUid, + enterprise_user_id: EnterpriseDataManager.toNumber(userId), + }); } - case Enterprise.EnterpriseDataEntity.USER_ALIASES: - target.user_aliases = (target.user_aliases || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeUserAliasChunk) - ) - break - case Enterprise.EnterpriseDataEntity.ROLE_PRIVILEGES: - target.role_privileges = (target.role_privileges || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeRolePrivilegeChunk) - ) - break - case Enterprise.EnterpriseDataEntity.MANAGED_NODES: - target.managed_nodes = (target.managed_nodes || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeManagedNodeChunk) - ) - break - case Enterprise.EnterpriseDataEntity.ROLE_ENFORCEMENTS: - target.role_enforcements = (target.role_enforcements || []).concat( - EnterpriseDataManager.decodeChunk(data, EnterpriseDataManager.decodeRoleEnforcementChunk) - ) - break - default: - break + } catch {} } + target.queued_team_users = (target.queued_team_users || []).concat( + flatLinks, + ); + break; + } + case Enterprise.EnterpriseDataEntity.USER_ALIASES: + target.user_aliases = (target.user_aliases || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeUserAliasChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.ROLE_PRIVILEGES: + target.role_privileges = (target.role_privileges || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeRolePrivilegeChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.MANAGED_NODES: + target.managed_nodes = (target.managed_nodes || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeManagedNodeChunk, + ), + ); + break; + case Enterprise.EnterpriseDataEntity.ROLE_ENFORCEMENTS: + target.role_enforcements = (target.role_enforcements || []).concat( + EnterpriseDataManager.decodeChunk( + data, + EnterpriseDataManager.decodeRoleEnforcementChunk, + ), + ); + break; + default: + break; } -} \ No newline at end of file + } +} diff --git a/KeeperSdk/src/teams/listTeams.ts b/KeeperSdk/src/teams/listTeams.ts index 3d055cc5..4ffebd0a 100644 --- a/KeeperSdk/src/teams/listTeams.ts +++ b/KeeperSdk/src/teams/listTeams.ts @@ -1,498 +1,574 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { isNumber, TOKEN_SEPARATOR_PATTERN } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; +import { isNumber, TOKEN_SEPARATOR_PATTERN } from "../utils"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type DecryptedRoleNames, - type EnterpriseDisplayNames, - type EnterpriseNode, - type EnterpriseRole, - type EnterpriseTeamRecord, - type EnterpriseTeamUserLink, - type EnterpriseUser, - type GetEnterpriseDataResponse, -} from './enterpriseData' -import { fetchShareObjects, fetchTeamMemberEmails, resolvePrimaryEnterpriseId } from './shareTeams' + EnterpriseDataInclude, + EnterpriseDataManager, + type DecryptedRoleNames, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseTeamRecord, + type EnterpriseTeamUserLink, + type EnterpriseUser, + type GetEnterpriseDataResponse, +} from "./enterpriseData"; +import { + fetchShareObjects, + fetchTeamMemberEmails, + resolvePrimaryEnterpriseId, +} from "./shareTeams"; export enum TeamColumn { - Restricts = 'restricts', - Node = 'node', - UserCount = 'user_count', - Users = 'users', - RoleCount = 'role_count', - Roles = 'roles', + Restricts = "restricts", + Node = "node", + UserCount = "user_count", + Users = "users", + RoleCount = "role_count", + Roles = "roles", } -export type TeamColumnInput = TeamColumn | `${TeamColumn}` +export type TeamColumnInput = TeamColumn | `${TeamColumn}`; -export const SUPPORTED_TEAM_COLUMNS: readonly TeamColumn[] = Object.values(TeamColumn) +export const SUPPORTED_TEAM_COLUMNS: readonly TeamColumn[] = + Object.values(TeamColumn); export const DEFAULT_TEAM_COLUMNS: readonly TeamColumn[] = [ - TeamColumn.Restricts, - TeamColumn.Node, - TeamColumn.UserCount, - TeamColumn.RoleCount, -] + TeamColumn.Restricts, + TeamColumn.Node, + TeamColumn.UserCount, + TeamColumn.RoleCount, +]; -const NODE_PATH_SEPARATOR = '\\' -const MIN_ASCII_COL_WIDTH = 2 -const ALL_COLUMNS_WILDCARD = '*' +const NODE_PATH_SEPARATOR = "\\"; +const MIN_ASCII_COL_WIDTH = 2; +const ALL_COLUMNS_WILDCARD = "*"; const HEADER_BY_COLUMN: Record = { - [TeamColumn.Restricts]: 'Restricts', - [TeamColumn.Node]: 'Node', - [TeamColumn.UserCount]: 'User Count', - [TeamColumn.Users]: 'Users', - [TeamColumn.RoleCount]: 'Role Count', - [TeamColumn.Roles]: 'Roles', -} + [TeamColumn.Restricts]: "Restricts", + [TeamColumn.Node]: "Node", + [TeamColumn.UserCount]: "User Count", + [TeamColumn.Users]: "Users", + [TeamColumn.RoleCount]: "Role Count", + [TeamColumn.Roles]: "Roles", +}; -export type ListTeamSort = 'company' | 'team_uid' | 'name' +export type ListTeamSort = "company" | "team_uid" | "name"; export type ListTeamsOptions = { - pattern?: string | null - /** Show teams from all enterprises in contacts (Commander --all). Default: primary org only. */ - all?: boolean - /** Include Member column from enterprise cache (Commander -v). */ - verbose?: boolean - /** Fetch members via vault/get_team_members when not cached (Commander -vv). */ - veryVerbose?: boolean - sort?: ListTeamSort - /** Enterprise admin detail columns; when set, uses get_enterprise_data instead of share objects. */ - columns?: TeamColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null -} + pattern?: string | null; + /** Show teams from all enterprises in contacts (Commander --all). Default: primary org only. */ + all?: boolean; + /** Include Member column from enterprise cache (Commander -v). */ + verbose?: boolean; + /** Fetch members via vault/get_team_members when not cached (Commander -vv). */ + veryVerbose?: boolean; + sort?: ListTeamSort; + /** Enterprise admin detail columns; when set, uses get_enterprise_data instead of share objects. */ + columns?: TeamColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null; +}; export type ListTeamRow = { - team_uid: string - name: string - company?: string - enterprise_id?: number - member?: string - restricts?: string - node?: string - user_count?: number - users?: string[] - role_count?: number - roles?: string[] -} + team_uid: string; + name: string; + company?: string; + enterprise_id?: number; + member?: string; + restricts?: string; + node?: string; + user_count?: number; + users?: string[]; + role_count?: number; + roles?: string[]; +}; export type FormattedTeamsTable = { - headers: string[] - rows: string[][] -} + headers: string[]; + rows: string[][]; +}; export type FormatTeamsTableOptions = { - columns?: ListTeamsOptions['columns'] - /** Commander -v / -vv: show Member column. */ - showMember?: boolean - style?: 'commander' | 'enterprise' -} + columns?: ListTeamsOptions["columns"]; + /** Commander -v / -vv: show Member column. */ + showMember?: boolean; + style?: "commander" | "enterprise"; +}; type DecorateContext = { - nodePaths: Map - teamUsers: Map> - roleTeams: Map> - usernameById: Map - roleNameById: Map -} + nodePaths: Map; + teamUsers: Map>; + roleTeams: Map>; + usernameById: Map; + roleNameById: Map; +}; export function formatTeamRestricts(team: EnterpriseTeamRecord): string { - const r = team.restrict_view === true ? 'R ' : ' ' - const w = team.restrict_edit === true ? 'W ' : ' ' - const restrictShare = team.restrict_share === true || team.restrict_sharing === true - const s = restrictShare ? 'S' : ' ' - return r + w + s + const r = team.restrict_view === true ? "R " : " "; + const w = team.restrict_edit === true ? "W " : " "; + const restrictShare = + team.restrict_share === true || team.restrict_sharing === true; + const s = restrictShare ? "S" : " "; + return r + w + s; } -export async function listTeams(auth: Auth, options: ListTeamsOptions = {}): Promise { - if (options.columns != null) { - return listTeamsEnterprise(auth, options) - } - return listTeamsCommander(auth, options) +export async function listTeams( + auth: Auth, + options: ListTeamsOptions = {}, +): Promise { + if (options.columns != null) { + return listTeamsEnterprise(auth, options); + } + return listTeamsCommander(auth, options); } -async function listTeamsCommander(auth: Auth, options: ListTeamsOptions): Promise { - const [shareObjects, primaryEnterpriseId] = await Promise.all([ - fetchShareObjects(auth), - resolvePrimaryEnterpriseId(auth), - ]) - const showAll = options.all === true - const pattern = options.pattern?.trim() || null - const showMembers = options.verbose === true || options.veryVerbose === true - - const rows: ListTeamRow[] = [] - for (const team of shareObjects.teams.values()) { - if (!showAll && primaryEnterpriseId != null && team.enterprise_id !== primaryEnterpriseId) { - continue - } - const company = - team.enterprise_id != null - ? shareObjects.enterprises.get(team.enterprise_id) || '' - : '' - const row: ListTeamRow = { - team_uid: team.team_uid, - name: team.name, - company, - enterprise_id: team.enterprise_id, - } - if (pattern && !rowMatchesPattern(row, pattern)) continue - rows.push(row) +async function listTeamsCommander( + auth: Auth, + options: ListTeamsOptions, +): Promise { + const [shareObjects, primaryEnterpriseId] = await Promise.all([ + fetchShareObjects(auth), + resolvePrimaryEnterpriseId(auth), + ]); + const showAll = options.all === true; + const pattern = options.pattern?.trim() || null; + const showMembers = options.verbose === true || options.veryVerbose === true; + + const rows: ListTeamRow[] = []; + for (const team of shareObjects.teams.values()) { + if ( + !showAll && + primaryEnterpriseId != null && + team.enterprise_id !== primaryEnterpriseId + ) { + continue; } - - if (showMembers) { - await attachCommanderTeamMembers(auth, rows, options.veryVerbose === true) - } - - sortCommanderRows(rows, options.sort || 'company') - return rows + const company = + team.enterprise_id != null + ? shareObjects.enterprises.get(team.enterprise_id) || "" + : ""; + const row: ListTeamRow = { + team_uid: team.team_uid, + name: team.name, + company, + enterprise_id: team.enterprise_id, + }; + if (pattern && !rowMatchesPattern(row, pattern)) continue; + rows.push(row); + } + + if (showMembers) { + await attachCommanderTeamMembers(auth, rows, options.veryVerbose === true); + } + + sortCommanderRows(rows, options.sort || "company"); + return rows; } -async function listTeamsEnterprise(auth: Auth, options: ListTeamsOptions): Promise { - const columns = resolveColumns(options.columns) - const includes = includesForColumns(columns) - const wantsDisplayNames = columns.includes(TeamColumn.Node) || columns.includes(TeamColumn.Roles) - - const enterpriseData = new EnterpriseDataManager(auth) - const emptyDisplayNames: EnterpriseDisplayNames = { nodes: new Map(), roles: new Map() } - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(includes), - wantsDisplayNames ? enterpriseData.getDisplayNames() : Promise.resolve(emptyDisplayNames), - ]) - - const teams = response.teams || [] - const nodes = response.nodes || [] - applyDecryptedNodeNames(nodes, displayNames.nodes) - - const context: DecorateContext = { - nodePaths: buildNodePathLookup(nodes), - teamUsers: buildTeamUserMap(response.team_users || []), - roleTeams: buildRoleTeamMap(response), - usernameById: buildUserUsernameMap(response.users || []), - roleNameById: buildRoleNameMap(response.roles || [], displayNames.roles), - } - - const pattern = options.pattern?.trim() || null - const rows: ListTeamRow[] = [] - for (const team of teams) { - const row: ListTeamRow = { - team_uid: team.team_uid, - name: teamDisplayName(team), - } - decorateRow(row, team, columns, context) - if (pattern && !rowMatchesPattern(row, pattern)) continue - rows.push(row) - } - - rows.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) - return rows +async function listTeamsEnterprise( + auth: Auth, + options: ListTeamsOptions, +): Promise { + const columns = resolveColumns(options.columns); + const includes = includesForColumns(columns); + const wantsDisplayNames = + columns.includes(TeamColumn.Node) || columns.includes(TeamColumn.Roles); + + const enterpriseData = new EnterpriseDataManager(auth); + const emptyDisplayNames: EnterpriseDisplayNames = { + nodes: new Map(), + roles: new Map(), + }; + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(includes), + wantsDisplayNames + ? enterpriseData.getDisplayNames() + : Promise.resolve(emptyDisplayNames), + ]); + + const teams = response.teams || []; + const nodes = response.nodes || []; + applyDecryptedNodeNames(nodes, displayNames.nodes); + + const context: DecorateContext = { + nodePaths: buildNodePathLookup(nodes), + teamUsers: buildTeamUserMap(response.team_users || []), + roleTeams: buildRoleTeamMap(response), + usernameById: buildUserUsernameMap(response.users || []), + roleNameById: buildRoleNameMap(response.roles || [], displayNames.roles), + }; + + const pattern = options.pattern?.trim() || null; + const rows: ListTeamRow[] = []; + for (const team of teams) { + const row: ListTeamRow = { + team_uid: team.team_uid, + name: teamDisplayName(team), + }; + decorateRow(row, team, columns, context); + if (pattern && !rowMatchesPattern(row, pattern)) continue; + rows.push(row); + } + + rows.sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); + return rows; } export function formatTeamsTable( - rows: ListTeamRow[], - options: FormatTeamsTableOptions = {} + rows: ListTeamRow[], + options: FormatTeamsTableOptions = {}, ): FormattedTeamsTable { - const style = options.style ?? (options.columns != null ? 'enterprise' : 'commander') - if (style === 'commander') { - const showMember = options.showMember === true - const headers = ['#', 'Company', 'Team UID', 'Name'] - if (showMember) headers.push('Member') - const outRows = rows.map((row, rowIndex) => { - const cells = [ - String(rowIndex + 1), - row.company ?? '', - row.team_uid, - row.name, - ] - if (showMember) cells.push(row.member ?? '') - return cells - }) - return { headers, rows: outRows } - } - - const columns = resolveColumns(options.columns) - const headers: string[] = ['#', 'Team UID', 'Name', ...columns.map((column) => HEADER_BY_COLUMN[column])] - - const outRows: string[][] = rows.map((row, rowIndex) => { - const cells: string[] = [String(rowIndex + 1), row.team_uid, row.name] - for (const column of columns) cells.push(formatCell(row, column)) - return cells - }) - - return { headers, rows: outRows } + const style = + options.style ?? (options.columns != null ? "enterprise" : "commander"); + if (style === "commander") { + const showMember = options.showMember === true; + const headers = ["#", "Company", "Team UID", "Name"]; + if (showMember) headers.push("Member"); + const outRows = rows.map((row, rowIndex) => { + const cells = [ + String(rowIndex + 1), + row.company ?? "", + row.team_uid, + row.name, + ]; + if (showMember) cells.push(row.member ?? ""); + return cells; + }); + return { headers, rows: outRows }; + } + + const columns = resolveColumns(options.columns); + const headers: string[] = [ + "#", + "Team UID", + "Name", + ...columns.map((column) => HEADER_BY_COLUMN[column]), + ]; + + const outRows: string[][] = rows.map((row, rowIndex) => { + const cells: string[] = [String(rowIndex + 1), row.team_uid, row.name]; + for (const column of columns) cells.push(formatCell(row, column)); + return cells; + }); + + return { headers, rows: outRows }; } export function renderTeamsAsciiTable( - table: FormattedTeamsTable, - options: { minColWidth?: number } = {} + table: FormattedTeamsTable, + options: { minColWidth?: number } = {}, ): string { - const { minColWidth = MIN_ASCII_COL_WIDTH } = options - const { headers, rows } = table - const columnCount = headers.length - - const expandedRows: string[][][] = rows.map((row) => - row.map((cell) => (cell.includes('\n') ? cell.split('\n') : [cell])) - ) - - const columnWidths = new Array(columnCount).fill(0) + const { minColWidth = MIN_ASCII_COL_WIDTH } = options; + const { headers, rows } = table; + const columnCount = headers.length; + + const expandedRows: string[][][] = rows.map((row) => + row.map((cell) => (cell.includes("\n") ? cell.split("\n") : [cell])), + ); + + const columnWidths = new Array(columnCount).fill(0); + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { + columnWidths[columnIndex] = Math.max( + headers[columnIndex].length, + minColWidth, + ); + } + for (const row of expandedRows) { for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { - columnWidths[columnIndex] = Math.max(headers[columnIndex].length, minColWidth) + for (const line of row[columnIndex]) { + columnWidths[columnIndex] = Math.max( + columnWidths[columnIndex], + line.length, + minColWidth, + ); + } } - for (const row of expandedRows) { - for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { - for (const line of row[columnIndex]) { - columnWidths[columnIndex] = Math.max(columnWidths[columnIndex], line.length, minColWidth) - } - } + } + + const padCell = (cell: string, columnIndex: number): string => + cell + " ".repeat(columnWidths[columnIndex] - cell.length); + const formatPhysicalRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const ruleRow = formatPhysicalRow( + columnWidths.map((width) => "-".repeat(width)), + ); + const lines: string[] = [formatPhysicalRow(headers), ruleRow]; + + for (const row of expandedRows) { + const physicalLineCount = Math.max(...row.map((cell) => cell.length)); + for (let lineIndex = 0; lineIndex < physicalLineCount; lineIndex += 1) { + const physicalCells: string[] = row.map((cell) => cell[lineIndex] ?? ""); + lines.push(formatPhysicalRow(physicalCells)); } - - const padCell = (cell: string, columnIndex: number): string => - cell + ' '.repeat(columnWidths[columnIndex] - cell.length) - const formatPhysicalRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const ruleRow = formatPhysicalRow(columnWidths.map((width) => '-'.repeat(width))) - const lines: string[] = [formatPhysicalRow(headers), ruleRow] - - for (const row of expandedRows) { - const physicalLineCount = Math.max(...row.map((cell) => cell.length)) - for (let lineIndex = 0; lineIndex < physicalLineCount; lineIndex += 1) { - const physicalCells: string[] = row.map((cell) => cell[lineIndex] ?? '') - lines.push(formatPhysicalRow(physicalCells)) - } - } - return lines.join('\n') + } + return lines.join("\n"); } -function includesForColumns(columns: readonly TeamColumn[]): EnterpriseDataInclude[] { - const set = new Set([EnterpriseDataInclude.Teams]) - for (const column of columns) { - switch (column) { - case TeamColumn.Node: - set.add(EnterpriseDataInclude.Nodes) - break - case TeamColumn.UserCount: - case TeamColumn.Users: - set.add(EnterpriseDataInclude.TeamUsers) - if (column === TeamColumn.Users) set.add(EnterpriseDataInclude.Users) - break - case TeamColumn.RoleCount: - case TeamColumn.Roles: - set.add(EnterpriseDataInclude.RoleTeams) - if (column === TeamColumn.Roles) set.add(EnterpriseDataInclude.Roles) - break - } +function includesForColumns( + columns: readonly TeamColumn[], +): EnterpriseDataInclude[] { + const set = new Set([EnterpriseDataInclude.Teams]); + for (const column of columns) { + switch (column) { + case TeamColumn.Node: + set.add(EnterpriseDataInclude.Nodes); + break; + case TeamColumn.UserCount: + case TeamColumn.Users: + set.add(EnterpriseDataInclude.TeamUsers); + if (column === TeamColumn.Users) set.add(EnterpriseDataInclude.Users); + break; + case TeamColumn.RoleCount: + case TeamColumn.Roles: + set.add(EnterpriseDataInclude.RoleTeams); + if (column === TeamColumn.Roles) set.add(EnterpriseDataInclude.Roles); + break; } - return Array.from(set) + } + return Array.from(set); } -function resolveColumns(input: ListTeamsOptions['columns']): TeamColumn[] { - if (input == null) return [...DEFAULT_TEAM_COLUMNS] - if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_TEAM_COLUMNS] - - const requested = Array.isArray(input) ? input : input.split(',').map((part) => part.trim()) - const allowed = new Set(SUPPORTED_TEAM_COLUMNS) - const seen = new Set() - for (const column of requested) { - if (column && allowed.has(column)) seen.add(column as TeamColumn) - } - if (seen.size === 0) return [...DEFAULT_TEAM_COLUMNS] - return SUPPORTED_TEAM_COLUMNS.filter((column) => seen.has(column)) +function resolveColumns(input: ListTeamsOptions["columns"]): TeamColumn[] { + if (input == null) return [...DEFAULT_TEAM_COLUMNS]; + if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_TEAM_COLUMNS]; + + const requested = Array.isArray(input) + ? input + : input.split(",").map((part) => part.trim()); + const allowed = new Set(SUPPORTED_TEAM_COLUMNS); + const seen = new Set(); + for (const column of requested) { + if (column && allowed.has(column)) seen.add(column as TeamColumn); + } + if (seen.size === 0) return [...DEFAULT_TEAM_COLUMNS]; + return SUPPORTED_TEAM_COLUMNS.filter((column) => seen.has(column)); } function teamDisplayName(team: { name?: string; team_uid: string }): string { - return (team.name || team.team_uid || '').trim() || team.team_uid + return (team.name || team.team_uid || "").trim() || team.team_uid; } function tokenize(text: string): string[] { - return text.split(TOKEN_SEPARATOR_PATTERN).filter((token) => token.length > 0) + return text + .split(TOKEN_SEPARATOR_PATTERN) + .filter((token) => token.length > 0); } async function attachCommanderTeamMembers( - auth: Auth, - rows: ListTeamRow[], - fetchMissing: boolean + auth: Auth, + rows: ListTeamRow[], + fetchMissing: boolean, ): Promise { - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData([ - EnterpriseDataInclude.TeamUsers, - EnterpriseDataInclude.Users, - ]) - const teamUsers = buildTeamUserMap(response.team_users || []) - const usernameById = buildUserUsernameMap(response.users || []) - - for (const row of rows) { - const ids = teamUsers.get(row.team_uid) - if (ids && ids.size > 0) { - row.member = resolveSortedNames(ids, usernameById).join('\n') - continue - } - if (!fetchMissing) continue - try { - const emails = await fetchTeamMemberEmails(auth, row.team_uid) - if (emails.length > 0) row.member = emails.join('\n') - } catch { - /* best-effort; leave member blank */ - } + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData([ + EnterpriseDataInclude.TeamUsers, + EnterpriseDataInclude.Users, + ]); + const teamUsers = buildTeamUserMap(response.team_users || []); + const usernameById = buildUserUsernameMap(response.users || []); + + for (const row of rows) { + const ids = teamUsers.get(row.team_uid); + if (ids && ids.size > 0) { + row.member = resolveSortedNames(ids, usernameById).join("\n"); + continue; + } + if (!fetchMissing) continue; + try { + const emails = await fetchTeamMemberEmails(auth, row.team_uid); + if (emails.length > 0) row.member = emails.join("\n"); + } catch { + /* best-effort; leave member blank */ } + } } function sortCommanderRows(rows: ListTeamRow[], sort: ListTeamSort): void { - if (sort === 'team_uid') { - rows.sort((a, b) => a.team_uid.localeCompare(b.team_uid, undefined, { sensitivity: 'base' })) - return - } - if (sort === 'name') { - rows.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) - return - } - rows.sort((a, b) => { - const companyCmp = (a.company || '').localeCompare(b.company || '', undefined, { sensitivity: 'base' }) - if (companyCmp !== 0) return companyCmp - return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) - }) + if (sort === "team_uid") { + rows.sort((a, b) => + a.team_uid.localeCompare(b.team_uid, undefined, { sensitivity: "base" }), + ); + return; + } + if (sort === "name") { + rows.sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); + return; + } + rows.sort((a, b) => { + const companyCmp = (a.company || "").localeCompare( + b.company || "", + undefined, + { sensitivity: "base" }, + ); + if (companyCmp !== 0) return companyCmp; + return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); + }); } function rowMatchesPattern(row: ListTeamRow, pattern: string): boolean { - const lowered = pattern.toLowerCase() - const tokens: string[] = [] - tokens.push(row.team_uid.toLowerCase()) - tokens.push(...tokenize(row.name.toLowerCase())) - if (row.company) tokens.push(...tokenize(row.company.toLowerCase())) - if (row.member) tokens.push(...tokenize(row.member.toLowerCase())) - if (row.restricts) tokens.push(row.restricts.toLowerCase()) - if (row.node) tokens.push(...tokenize(row.node.toLowerCase())) - if (row.user_count != null) tokens.push(String(row.user_count)) - if (row.role_count != null) tokens.push(String(row.role_count)) - for (const list of [row.users, row.roles]) { - if (!list) continue - for (const value of list) tokens.push(...tokenize(value.toLowerCase())) - } - return tokens.some((token) => token.includes(lowered)) + const lowered = pattern.toLowerCase(); + const tokens: string[] = []; + tokens.push(row.team_uid.toLowerCase()); + tokens.push(...tokenize(row.name.toLowerCase())); + if (row.company) tokens.push(...tokenize(row.company.toLowerCase())); + if (row.member) tokens.push(...tokenize(row.member.toLowerCase())); + if (row.restricts) tokens.push(row.restricts.toLowerCase()); + if (row.node) tokens.push(...tokenize(row.node.toLowerCase())); + if (row.user_count != null) tokens.push(String(row.user_count)); + if (row.role_count != null) tokens.push(String(row.role_count)); + for (const list of [row.users, row.roles]) { + if (!list) continue; + for (const value of list) tokens.push(...tokenize(value.toLowerCase())); + } + return tokens.some((token) => token.includes(lowered)); } -function applyDecryptedNodeNames(nodes: EnterpriseNode[], decrypted: Map): void { - if (decrypted.size === 0) return - for (const node of nodes) { - const display = decrypted.get(node.node_id) - if (display) node.displayName = display - } +function applyDecryptedNodeNames( + nodes: EnterpriseNode[], + decrypted: Map, +): void { + if (decrypted.size === 0) return; + for (const node of nodes) { + const display = decrypted.get(node.node_id); + if (display) node.displayName = display; + } } function buildNodePathLookup(nodes: EnterpriseNode[]): Map { - return new Map( - nodes.map((node) => [ - node.node_id, - EnterpriseDataManager.getNodePath(nodes, node.node_id, { separator: NODE_PATH_SEPARATOR }), - ]) - ) + return new Map( + nodes.map((node) => [ + node.node_id, + EnterpriseDataManager.getNodePath(nodes, node.node_id, { + separator: NODE_PATH_SEPARATOR, + }), + ]), + ); } -function buildTeamUserMap(links: EnterpriseTeamUserLink[]): Map> { - const byTeam = new Map>() - for (const link of links) { - if (!link.team_uid) continue - const set = byTeam.get(link.team_uid) - if (set) set.add(link.enterprise_user_id) - else byTeam.set(link.team_uid, new Set([link.enterprise_user_id])) - } - return byTeam +function buildTeamUserMap( + links: EnterpriseTeamUserLink[], +): Map> { + const byTeam = new Map>(); + for (const link of links) { + if (!link.team_uid) continue; + const set = byTeam.get(link.team_uid); + if (set) set.add(link.enterprise_user_id); + else byTeam.set(link.team_uid, new Set([link.enterprise_user_id])); + } + return byTeam; } -function buildRoleTeamMap(response: GetEnterpriseDataResponse): Map> { - const map = new Map>() - for (const link of response.role_teams || []) { - if (!link.team_uid) continue - const set = map.get(link.team_uid) - if (set) set.add(link.role_id) - else map.set(link.team_uid, new Set([link.role_id])) - } - return map +function buildRoleTeamMap( + response: GetEnterpriseDataResponse, +): Map> { + const map = new Map>(); + for (const link of response.role_teams || []) { + if (!link.team_uid) continue; + const set = map.get(link.team_uid); + if (set) set.add(link.role_id); + else map.set(link.team_uid, new Set([link.role_id])); + } + return map; } function buildUserUsernameMap(users: EnterpriseUser[]): Map { - const map = new Map() - for (const user of users) { - if (isNumber(user.enterprise_user_id ) && user.username) { - map.set(user.enterprise_user_id, user.username) - } + const map = new Map(); + for (const user of users) { + if (isNumber(user.enterprise_user_id) && user.username) { + map.set(user.enterprise_user_id, user.username); } - return map + } + return map; } -function buildRoleNameMap(roles: EnterpriseRole[], decrypted: DecryptedRoleNames): Map { - const map = new Map() - for (const role of roles) { - if (isNumber(role.role_id)) continue - const display = (role.displayName || decrypted.get(role.role_id) || '').trim() - map.set(role.role_id, display || String(role.role_id)) - } - return map +function buildRoleNameMap( + roles: EnterpriseRole[], + decrypted: DecryptedRoleNames, +): Map { + const map = new Map(); + for (const role of roles) { + if (isNumber(role.role_id)) continue; + const display = ( + role.displayName || + decrypted.get(role.role_id) || + "" + ).trim(); + map.set(role.role_id, display || String(role.role_id)); + } + return map; } -function resolveSortedNames(ids: Set, nameById: Map): string[] { - const result: string[] = [] - for (const id of ids) { - const name = nameById.get(id) - if (name) result.push(name) - } - result.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) - return result +function resolveSortedNames( + ids: Set, + nameById: Map, +): string[] { + const result: string[] = []; + for (const id of ids) { + const name = nameById.get(id); + if (name) result.push(name); + } + result.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); + return result; } function decorateRow( - row: ListTeamRow, - team: EnterpriseTeamRecord, - columns: readonly TeamColumn[], - context: DecorateContext + row: ListTeamRow, + team: EnterpriseTeamRecord, + columns: readonly TeamColumn[], + context: DecorateContext, ): void { - const { nodePaths, teamUsers, roleTeams, usernameById, roleNameById } = context - - for (const column of columns) { - switch (column) { - case TeamColumn.Restricts: - row.restricts = formatTeamRestricts(team) - break - case TeamColumn.Node: - row.node = nodePaths.get(team.node_id) || '' - break - case TeamColumn.UserCount: - row.user_count = teamUsers.get(team.team_uid)?.size ?? 0 - break - case TeamColumn.Users: { - const ids = teamUsers.get(team.team_uid) - row.users = ids ? resolveSortedNames(ids, usernameById) : [] - break - } - case TeamColumn.RoleCount: - row.role_count = roleTeams.get(team.team_uid)?.size ?? 0 - break - case TeamColumn.Roles: { - const ids = roleTeams.get(team.team_uid) - row.roles = ids ? resolveSortedNames(ids, roleNameById) : [] - break - } - } + const { nodePaths, teamUsers, roleTeams, usernameById, roleNameById } = + context; + + for (const column of columns) { + switch (column) { + case TeamColumn.Restricts: + row.restricts = formatTeamRestricts(team); + break; + case TeamColumn.Node: + row.node = nodePaths.get(team.node_id) || ""; + break; + case TeamColumn.UserCount: + row.user_count = teamUsers.get(team.team_uid)?.size ?? 0; + break; + case TeamColumn.Users: { + const ids = teamUsers.get(team.team_uid); + row.users = ids ? resolveSortedNames(ids, usernameById) : []; + break; + } + case TeamColumn.RoleCount: + row.role_count = roleTeams.get(team.team_uid)?.size ?? 0; + break; + case TeamColumn.Roles: { + const ids = roleTeams.get(team.team_uid); + row.roles = ids ? resolveSortedNames(ids, roleNameById) : []; + break; + } } + } } function formatListCell(values: string[] | undefined): string { - if (!values || values.length === 0) return '' - return values.join('\n') + if (!values || values.length === 0) return ""; + return values.join("\n"); } function formatCell(row: ListTeamRow, column: TeamColumn): string { - switch (column) { - case TeamColumn.Restricts: - return row.restricts ?? '' - case TeamColumn.Node: - return row.node ?? '' - case TeamColumn.UserCount: - return row.user_count == null ? '' : String(row.user_count) - case TeamColumn.Users: - return formatListCell(row.users) - case TeamColumn.RoleCount: - return row.role_count == null ? '' : String(row.role_count) - case TeamColumn.Roles: - return formatListCell(row.roles) - } + switch (column) { + case TeamColumn.Restricts: + return row.restricts ?? ""; + case TeamColumn.Node: + return row.node ?? ""; + case TeamColumn.UserCount: + return row.user_count == null ? "" : String(row.user_count); + case TeamColumn.Users: + return formatListCell(row.users); + case TeamColumn.RoleCount: + return row.role_count == null ? "" : String(row.role_count); + case TeamColumn.Roles: + return formatListCell(row.roles); + } } diff --git a/KeeperSdk/src/teams/shareTeams.ts b/KeeperSdk/src/teams/shareTeams.ts index 0164bb95..edbac52f 100644 --- a/KeeperSdk/src/teams/shareTeams.ts +++ b/KeeperSdk/src/teams/shareTeams.ts @@ -1,79 +1,90 @@ import { - getShareObjectsMessage, - getTeamMembersMessage, - normal64Bytes, - webSafe64FromBytes, - type Auth, - type Records, -} from '@keeper-security/keeperapi' + getShareObjectsMessage, + getTeamMembersMessage, + normal64Bytes, + webSafe64FromBytes, + type Auth, + type Records, +} from "@keeper-security/keeperapi"; export type ShareTeamEntry = { - team_uid: string - name: string - enterprise_id?: number -} + team_uid: string; + name: string; + enterprise_id?: number; +}; export type ShareObjectsSnapshot = { - enterprises: Map - teams: Map -} + enterprises: Map; + teams: Map; +}; function teamUidFromShareTeam(team: Records.IShareTeam): string | null { - if (!team.teamUid) return null - if (team.teamUid instanceof Uint8Array) return webSafe64FromBytes(team.teamUid) - if (typeof team.teamUid === 'string') return team.teamUid - return null + if (!team.teamUid) return null; + if (team.teamUid instanceof Uint8Array) + return webSafe64FromBytes(team.teamUid); + if (typeof team.teamUid === "string") return team.teamUid; + return null; } -function addShareTeams(target: Map, list: Records.IShareTeam[] | null | undefined): void { - for (const team of list || []) { - const team_uid = teamUidFromShareTeam(team) - if (!team_uid) continue - target.set(team_uid, { - team_uid, - name: (team.teamname || team_uid).trim() || team_uid, - enterprise_id: team.enterpriseId ?? undefined, - }) - } +function addShareTeams( + target: Map, + list: Records.IShareTeam[] | null | undefined, +): void { + for (const team of list || []) { + const team_uid = teamUidFromShareTeam(team); + if (!team_uid) continue; + target.set(team_uid, { + team_uid, + name: (team.teamname || team_uid).trim() || team_uid, + enterprise_id: team.enterpriseId ?? undefined, + }); + } } /** Teams and enterprise names from vault/get_share_objects (Keeper Commander contacts cache). */ -export async function fetchShareObjects(auth: Auth): Promise { - const response = await auth.executeRest(getShareObjectsMessage({})) - const enterprises = new Map() - for (const entry of response.shareEnterpriseNames || []) { - if (entry.enterpriseId == null) continue - enterprises.set(entry.enterpriseId, (entry.enterprisename || '').trim()) - } +export async function fetchShareObjects( + auth: Auth, +): Promise { + const response = await auth.executeRest(getShareObjectsMessage({})); + const enterprises = new Map(); + for (const entry of response.shareEnterpriseNames || []) { + if (entry.enterpriseId == null) continue; + enterprises.set(entry.enterpriseId, (entry.enterprisename || "").trim()); + } - const teams = new Map() - addShareTeams(teams, response.shareTeams) - addShareTeams(teams, response.shareMCTeams) - return { enterprises, teams } + const teams = new Map(); + addShareTeams(teams, response.shareTeams); + addShareTeams(teams, response.shareMCTeams); + return { enterprises, teams }; } -export async function resolvePrimaryEnterpriseId(auth: Auth): Promise { - let enterpriseId = auth.accountSummary?.license?.enterpriseId ?? undefined - if (enterpriseId != null) return enterpriseId - try { - await auth.loadAccountSummary() - } catch { - return undefined - } - return auth.accountSummary?.license?.enterpriseId ?? undefined +export async function resolvePrimaryEnterpriseId( + auth: Auth, +): Promise { + let enterpriseId = auth.accountSummary?.license?.enterpriseId ?? undefined; + if (enterpriseId != null) return enterpriseId; + try { + await auth.loadAccountSummary(); + } catch { + return undefined; + } + return auth.accountSummary?.license?.enterpriseId ?? undefined; } -export async function fetchTeamMemberEmails(auth: Auth, teamUid: string): Promise { - const response = await auth.executeRest( - getTeamMembersMessage({ - teamUid: normal64Bytes(teamUid), - }) - ) - const emails: string[] = [] - for (const user of response.enterpriseUser || []) { - const email = (user.email || user.enterpriseUsername || '').trim() - if (email) emails.push(email) - } - emails.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) - return emails +export async function fetchTeamMemberEmails( + auth: Auth, + teamUid: string, +): Promise { + const response = await auth.executeRest( + getTeamMembersMessage({ + teamUid: normal64Bytes(teamUid), + }), + ); + const emails: string[] = []; + for (const user of response.enterpriseUser || []) { + const email = (user.email || user.enterpriseUsername || "").trim(); + if (email) emails.push(email); + } + emails.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); + return emails; } diff --git a/KeeperSdk/src/teams/teamUtils.ts b/KeeperSdk/src/teams/teamUtils.ts index 35cb89d9..ccd9acdf 100644 --- a/KeeperSdk/src/teams/teamUtils.ts +++ b/KeeperSdk/src/teams/teamUtils.ts @@ -1,205 +1,235 @@ -import { isNumber, KeeperSdkError, ResultCodes } from '../utils' -import { EnterpriseDataManager, type EnterpriseNode, type EnterpriseTeamRecord } from './enterpriseData' - -export const NODE_PATH_SEPARATOR = '\\' -export const MAX_TEAM_NAME_LENGTH = 100 -export const TEAM_TABLE_HEADERS = ['#', 'Status', 'Team Name', 'Team UID', 'Node ID', 'Detail'] +import { isNumber, KeeperSdkError, ResultCodes } from "../utils"; +import { + EnterpriseDataManager, + type EnterpriseNode, + type EnterpriseTeamRecord, +} from "./enterpriseData"; + +export const NODE_PATH_SEPARATOR = "\\"; +export const MAX_TEAM_NAME_LENGTH = 100; +export const TEAM_TABLE_HEADERS = [ + "#", + "Status", + "Team Name", + "Team UID", + "Node ID", + "Detail", +]; export function validateTeamName(name: string): void { - if (name.length > MAX_TEAM_NAME_LENGTH) { - throw new KeeperSdkError( - `Team name exceeds ${MAX_TEAM_NAME_LENGTH} characters: "${name.substring(0, 30)}..."`, - ResultCodes.TEAM_NAME_TOO_LONG - ) - } + if (name.length > MAX_TEAM_NAME_LENGTH) { + throw new KeeperSdkError( + `Team name exceeds ${MAX_TEAM_NAME_LENGTH} characters: "${name.substring(0, 30)}..."`, + ResultCodes.TEAM_NAME_TOO_LONG, + ); + } } export function resolveExistingTeams( - teams: EnterpriseTeamRecord[], - identifiers: string[], - queuedTeams: EnterpriseTeamRecord[] = [] + teams: EnterpriseTeamRecord[], + identifiers: string[], + queuedTeams: EnterpriseTeamRecord[] = [], ): EnterpriseTeamRecord[] { - const byUid = new Map() - const byLowerName = new Map() - for (const team of teams) { - if (!team.team_uid) continue - byUid.set(team.team_uid, team) - const key = (team.name || '').trim().toLowerCase() - if (!key) continue - const existing = byLowerName.get(key) - if (existing) existing.push(team) - else byLowerName.set(key, [team]) - } - for (const team of queuedTeams) { - if (!team.team_uid || byUid.has(team.team_uid)) continue - const resolved: EnterpriseTeamRecord = { - team_uid: team.team_uid, - name: team.name, - node_id: team.node_id ?? 0, - ...(team.encrypted_team_key ? { encrypted_team_key: team.encrypted_team_key } : {}), - } - byUid.set(team.team_uid, resolved) - const key = (team.name || '').trim().toLowerCase() - if (!key || byLowerName.has(key)) continue - byLowerName.set(key, [resolved]) - } - - const found = new Map() - const missing: string[] = [] - for (const identifier of identifiers) { - const uidMatch = byUid.get(identifier) - if (uidMatch) { - found.set(uidMatch.team_uid, uidMatch) - continue - } - const nameMatches = byLowerName.get(identifier.toLowerCase()) - if (!nameMatches || nameMatches.length === 0) { - missing.push(identifier) - continue - } - if (nameMatches.length > 1) { - throw new KeeperSdkError( - `Team name "${identifier}" is not unique. Use Team UID.`, - ResultCodes.MULTIPLE_TEAM_MATCHES - ) - } - found.set(nameMatches[0].team_uid, nameMatches[0]) - } - - if (missing.length > 0) { - throw new KeeperSdkError( - `Team name(s) "${missing.join(', ')}" could not be resolved.`, - ResultCodes.TEAM_NOT_FOUND - ) - } - return Array.from(found.values()) + const byUid = new Map(); + const byLowerName = new Map(); + for (const team of teams) { + if (!team.team_uid) continue; + byUid.set(team.team_uid, team); + const key = (team.name || "").trim().toLowerCase(); + if (!key) continue; + const existing = byLowerName.get(key); + if (existing) existing.push(team); + else byLowerName.set(key, [team]); + } + for (const team of queuedTeams) { + if (!team.team_uid || byUid.has(team.team_uid)) continue; + const resolved: EnterpriseTeamRecord = { + team_uid: team.team_uid, + name: team.name, + node_id: team.node_id ?? 0, + ...(team.encrypted_team_key + ? { encrypted_team_key: team.encrypted_team_key } + : {}), + }; + byUid.set(team.team_uid, resolved); + const key = (team.name || "").trim().toLowerCase(); + if (!key || byLowerName.has(key)) continue; + byLowerName.set(key, [resolved]); + } + + const found = new Map(); + const missing: string[] = []; + for (const identifier of identifiers) { + const uidMatch = byUid.get(identifier); + if (uidMatch) { + found.set(uidMatch.team_uid, uidMatch); + continue; + } + const nameMatches = byLowerName.get(identifier.toLowerCase()); + if (!nameMatches || nameMatches.length === 0) { + missing.push(identifier); + continue; + } + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Team name "${identifier}" is not unique. Use Team UID.`, + ResultCodes.MULTIPLE_TEAM_MATCHES, + ); + } + found.set(nameMatches[0].team_uid, nameMatches[0]); + } + + if (missing.length > 0) { + throw new KeeperSdkError( + `Team name(s) "${missing.join(", ")}" could not be resolved.`, + ResultCodes.TEAM_NOT_FOUND, + ); + } + return Array.from(found.values()); } -function buildNodeNotFoundMessage(nodes: EnterpriseNode[], requested: string): string { - const lines: string[] = [`Parent node "${requested}" was not found.`] - if (nodes.length === 0) { - lines.push('No enterprise nodes are visible to the current user.') - return lines.join(' ') - } - const sorted = [...nodes].sort((a, b) => a.node_id - b.node_id) - const visible = sorted.slice(0, 25) - lines.push('Available nodes (id parent_id name):') - for (const node of visible) { - const own = (node.displayName || '').trim() || '' - const parent = node.parent_id || 0 - lines.push(` ${node.node_id} ${parent} ${own}`) - } - if (sorted.length > visible.length) { - lines.push(` ...and ${sorted.length - visible.length} more`) - } - return lines.join('\n') +function buildNodeNotFoundMessage( + nodes: EnterpriseNode[], + requested: string, +): string { + const lines: string[] = [`Parent node "${requested}" was not found.`]; + if (nodes.length === 0) { + lines.push("No enterprise nodes are visible to the current user."); + return lines.join(" "); + } + const sorted = [...nodes].sort((a, b) => a.node_id - b.node_id); + const visible = sorted.slice(0, 25); + lines.push("Available nodes (id parent_id name):"); + for (const node of visible) { + const own = (node.displayName || "").trim() || ""; + const parent = node.parent_id || 0; + lines.push(` ${node.node_id} ${parent} ${own}`); + } + if (sorted.length > visible.length) { + lines.push(` ...and ${sorted.length - visible.length} more`); + } + return lines.join("\n"); } export function resolveParentNode( - nodes: EnterpriseNode[], - identifier: string | number | null + nodes: EnterpriseNode[], + identifier: string | number | null, ): EnterpriseNode { - if (identifier === null || identifier === undefined || identifier === '') { - const root = nodes.find((node) => !node.parent_id || node.parent_id === 0) - if (!root) { - throw new KeeperSdkError( - buildNodeNotFoundMessage(nodes, ''), - ResultCodes.PARENT_NODE_NOT_FOUND - ) - } - return root - } - - if (typeof identifier === 'number') { - if (!isNumber(identifier)) { - throw new KeeperSdkError( - `Parent node "${identifier}" is not a valid node id.`, - ResultCodes.PARENT_NODE_REQUIRED - ) - } - const byId = nodes.find((node) => node.node_id === identifier) - if (!byId) { - throw new KeeperSdkError( - buildNodeNotFoundMessage(nodes, String(identifier)), - ResultCodes.PARENT_NODE_NOT_FOUND - ) - } - return byId - } - - const trimmed = identifier.trim() - if (!trimmed) { - throw new KeeperSdkError('Parent node is required.', ResultCodes.PARENT_NODE_REQUIRED) - } - - const numeric = Number(trimmed) - if (Number.isFinite(numeric) && Number.isInteger(numeric)) { - const byId = nodes.find((node) => node.node_id === numeric) - if (byId) return byId - } - - const lowered = trimmed.toLowerCase() - const byExactName = nodes.filter((node) => (node.displayName || '').trim().toLowerCase() === lowered) - if (byExactName.length === 1) return byExactName[0] - if (byExactName.length > 1) { - throw new KeeperSdkError( - `Multiple nodes match name "${trimmed}". Specify the node id instead.`, - ResultCodes.MULTIPLE_PARENT_NODE_MATCHES - ) - } - - const byPath = nodes.filter((node) => { - const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { - omitRoot: false, - separator: NODE_PATH_SEPARATOR, - }) - return path.trim().toLowerCase() === lowered - }) - if (byPath.length === 1) return byPath[0] - if (byPath.length > 1) { - throw new KeeperSdkError( - `Multiple nodes match path "${trimmed}". Specify the node id instead.`, - ResultCodes.MULTIPLE_PARENT_NODE_MATCHES - ) - } - + if (identifier === null || identifier === undefined || identifier === "") { + const root = nodes.find((node) => !node.parent_id || node.parent_id === 0); + if (!root) { + throw new KeeperSdkError( + buildNodeNotFoundMessage(nodes, ""), + ResultCodes.PARENT_NODE_NOT_FOUND, + ); + } + return root; + } + + if (typeof identifier === "number") { + if (!isNumber(identifier)) { + throw new KeeperSdkError( + `Parent node "${identifier}" is not a valid node id.`, + ResultCodes.PARENT_NODE_REQUIRED, + ); + } + const byId = nodes.find((node) => node.node_id === identifier); + if (!byId) { + throw new KeeperSdkError( + buildNodeNotFoundMessage(nodes, String(identifier)), + ResultCodes.PARENT_NODE_NOT_FOUND, + ); + } + return byId; + } + + const trimmed = identifier.trim(); + if (!trimmed) { + throw new KeeperSdkError( + "Parent node is required.", + ResultCodes.PARENT_NODE_REQUIRED, + ); + } + + const numeric = Number(trimmed); + if (Number.isFinite(numeric) && Number.isInteger(numeric)) { + const byId = nodes.find((node) => node.node_id === numeric); + if (byId) return byId; + } + + const lowered = trimmed.toLowerCase(); + const byExactName = nodes.filter( + (node) => (node.displayName || "").trim().toLowerCase() === lowered, + ); + if (byExactName.length === 1) return byExactName[0]; + if (byExactName.length > 1) { throw new KeeperSdkError( - buildNodeNotFoundMessage(nodes, trimmed), - ResultCodes.PARENT_NODE_NOT_FOUND - ) + `Multiple nodes match name "${trimmed}". Specify the node id instead.`, + ResultCodes.MULTIPLE_PARENT_NODE_MATCHES, + ); + } + + const byPath = nodes.filter((node) => { + const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }); + return path.trim().toLowerCase() === lowered; + }); + if (byPath.length === 1) return byPath[0]; + if (byPath.length > 1) { + throw new KeeperSdkError( + `Multiple nodes match path "${trimmed}". Specify the node id instead.`, + ResultCodes.MULTIPLE_PARENT_NODE_MATCHES, + ); + } + + throw new KeeperSdkError( + buildNodeNotFoundMessage(nodes, trimmed), + ResultCodes.PARENT_NODE_NOT_FOUND, + ); } -export function applyDecryptedNodeNames(nodes: EnterpriseNode[], decrypted: Map): void { - if (decrypted.size === 0) return - for (const node of nodes) { - const display = decrypted.get(node.node_id) - if (display) node.displayName = display - } +export function applyDecryptedNodeNames( + nodes: EnterpriseNode[], + decrypted: Map, +): void { + if (decrypted.size === 0) return; + for (const node of nodes) { + const display = decrypted.get(node.node_id); + if (display) node.displayName = display; + } } export function applyEnterpriseNameToRoot( - nodes: EnterpriseNode[], - enterpriseName: string | undefined + nodes: EnterpriseNode[], + enterpriseName: string | undefined, ): void { - const fallback = (enterpriseName || '').trim() - if (!fallback) return - for (const node of nodes) { - const isRoot = !node.parent_id || node.parent_id === 0 - if (!isRoot) continue - if (!(node.displayName || '').trim()) { - node.displayName = fallback - } - } + const fallback = (enterpriseName || "").trim(); + if (!fallback) return; + for (const node of nodes) { + const isRoot = !node.parent_id || node.parent_id === 0; + if (!isRoot) continue; + if (!(node.displayName || "").trim()) { + node.displayName = fallback; + } + } } -export function parentNeedsNameLookup(parent: string | number | null | undefined): boolean { - if (parent === undefined || parent === null || parent === '') return false - if (typeof parent === 'number') return false - const trimmed = parent.trim() - if (!trimmed) return false - const numeric = Number(trimmed) - if (Number.isFinite(numeric) && Number.isInteger(numeric) && trimmed === String(numeric)) { - return false - } - return true +export function parentNeedsNameLookup( + parent: string | number | null | undefined, +): boolean { + if (parent === undefined || parent === null || parent === "") return false; + if (typeof parent === "number") return false; + const trimmed = parent.trim(); + if (!trimmed) return false; + const numeric = Number(trimmed); + if ( + Number.isFinite(numeric) && + Number.isInteger(numeric) && + trimmed === String(numeric) + ) { + return false; + } + return true; } diff --git a/KeeperSdk/src/teams/updateTeam.ts b/KeeperSdk/src/teams/updateTeam.ts index 50056b28..d54c6701 100644 --- a/KeeperSdk/src/teams/updateTeam.ts +++ b/KeeperSdk/src/teams/updateTeam.ts @@ -1,231 +1,257 @@ -import type { Auth, KeeperResponse, RestCommand } from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' +import type { + Auth, + KeeperResponse, + RestCommand, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; +import { EnterpriseDataInclude, EnterpriseDataManager } from "./enterpriseData"; +import { TeamRestriction, type TeamRestrictionInput } from "./addTeam"; import { - EnterpriseDataInclude, - EnterpriseDataManager, -} from './enterpriseData' -import { TeamRestriction, type TeamRestrictionInput } from './addTeam' -import { - applyDecryptedNodeNames, - applyEnterpriseNameToRoot, - parentNeedsNameLookup, - resolveExistingTeams, - resolveParentNode, - TEAM_TABLE_HEADERS, - validateTeamName, -} from './teamUtils' - -const TEAM_UPDATE_COMMAND = 'team_update' + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveExistingTeams, + resolveParentNode, + TEAM_TABLE_HEADERS, + validateTeamName, +} from "./teamUtils"; + +const TEAM_UPDATE_COMMAND = "team_update"; const UPDATE_TEAM_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Teams, -] + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Teams, +]; export enum UpdateTeamStatus { - Updated = 'updated', - Failed = 'failed', + Updated = "updated", + Failed = "failed", } export type UpdateTeamInput = { - teams: string[] - name?: string - parent?: string | number | null - restrictEdit?: TeamRestrictionInput - restrictShare?: TeamRestrictionInput - restrictView?: TeamRestrictionInput -} + teams: string[]; + name?: string; + parent?: string | number | null; + restrictEdit?: TeamRestrictionInput; + restrictShare?: TeamRestrictionInput; + restrictView?: TeamRestrictionInput; +}; export type UpdateTeamItemResult = { - teamUid: string - teamName: string - nodeId: number - status: UpdateTeamStatus - message?: string -} + teamUid: string; + teamName: string; + nodeId: number; + status: UpdateTeamStatus; + message?: string; +}; export type UpdateTeamResult = { - success: boolean - items: UpdateTeamItemResult[] - updated: number - failed: number -} + success: boolean; + items: UpdateTeamItemResult[]; + updated: number; + failed: number; +}; type TeamUpdateRequestPayload = { - team_uid: string - team_name: string - node_id: number - restrict_edit: boolean - restrict_share: boolean - restrict_view: boolean -} + team_uid: string; + team_name: string; + node_id: number; + restrict_edit: boolean; + restrict_share: boolean; + restrict_view: boolean; +}; -export async function updateTeams(auth: Auth, input: UpdateTeamInput): Promise { - const requestedIdentifiers = (input.teams || []) - .map((value) => (typeof value === 'string' ? value.trim() : '')) - .filter((value) => value.length > 0) +export async function updateTeams( + auth: Auth, + input: UpdateTeamInput, +): Promise { + const requestedIdentifiers = (input.teams || []) + .map((value) => (typeof value === "string" ? value.trim() : "")) + .filter((value) => value.length > 0); - if (requestedIdentifiers.length === 0) { - throw new KeeperSdkError('No teams to update.', ResultCodes.NO_TEAMS_TO_UPDATE) - } + if (requestedIdentifiers.length === 0) { + throw new KeeperSdkError( + "No teams to update.", + ResultCodes.NO_TEAMS_TO_UPDATE, + ); + } - const newName = input.name?.trim() ? input.name.trim() : undefined - if (newName) validateTeamName(newName) - if (newName && requestedIdentifiers.length > 1) { - throw new KeeperSdkError( - 'Cannot rename more than one team in a single update.', - ResultCodes.MULTIPLE_TEAM_RENAME_NOT_ALLOWED - ) - } + const newName = input.name?.trim() ? input.name.trim() : undefined; + if (newName) validateTeamName(newName); + if (newName && requestedIdentifiers.length > 1) { + throw new KeeperSdkError( + "Cannot rename more than one team in a single update.", + ResultCodes.MULTIPLE_TEAM_RENAME_NOT_ALLOWED, + ); + } - const restrictEditOverride = resolveOptionalRestriction(input.restrictEdit) - const restrictShareOverride = resolveOptionalRestriction(input.restrictShare) - const restrictViewOverride = resolveOptionalRestriction(input.restrictView) + const restrictEditOverride = resolveOptionalRestriction(input.restrictEdit); + const restrictShareOverride = resolveOptionalRestriction(input.restrictShare); + const restrictViewOverride = resolveOptionalRestriction(input.restrictView); - const parentIdentifier = input.parent - const needsNameLookup = parentNeedsNameLookup(parentIdentifier) + const parentIdentifier = input.parent; + const needsNameLookup = parentNeedsNameLookup(parentIdentifier); - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(UPDATE_TEAM_INCLUDES) - const nodes = response.nodes || [] - applyEnterpriseNameToRoot(nodes, response.enterprise_name) + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(UPDATE_TEAM_INCLUDES); + const nodes = response.nodes || []; + applyEnterpriseNameToRoot(nodes, response.enterprise_name); - if (needsNameLookup) { - const displayNames = await enterpriseData.getDisplayNames() - applyDecryptedNodeNames(nodes, displayNames.nodes) - await enterpriseData.decryptNodeNames(nodes) - } + if (needsNameLookup) { + const displayNames = await enterpriseData.getDisplayNames(); + applyDecryptedNodeNames(nodes, displayNames.nodes); + await enterpriseData.decryptNodeNames(nodes); + } + + const allTeams = response.teams || []; + const resolvedTeams = resolveExistingTeams(allTeams, requestedIdentifiers); - const allTeams = response.teams || [] - const resolvedTeams = resolveExistingTeams(allTeams, requestedIdentifiers) - - let overrideNodeId: number | null = null - if (parentIdentifier !== undefined && parentIdentifier !== null && parentIdentifier !== '') { - try { - overrideNodeId = resolveParentNode(nodes, parentIdentifier).node_id - } catch (err) { - throw new KeeperSdkError( - extractErrorMessage(err), - ResultCodes.PARENT_NODE_NOT_FOUND - ) - } + let overrideNodeId: number | null = null; + if ( + parentIdentifier !== undefined && + parentIdentifier !== null && + parentIdentifier !== "" + ) { + try { + overrideNodeId = resolveParentNode(nodes, parentIdentifier).node_id; + } catch (err) { + throw new KeeperSdkError( + extractErrorMessage(err), + ResultCodes.PARENT_NODE_NOT_FOUND, + ); } + } - const items: UpdateTeamItemResult[] = [] - for (const team of resolvedTeams) { - const targetNodeId = overrideNodeId ?? team.node_id - const payload: TeamUpdateRequestPayload = { - team_uid: team.team_uid, - team_name: newName || team.name || '', - node_id: targetNodeId, - restrict_edit: restrictEditOverride ?? team.restrict_edit === true, - restrict_share: restrictShareOverride ?? (team.restrict_share === true || team.restrict_sharing === true), - restrict_view: restrictViewOverride ?? team.restrict_view === true, - } - - try { - await sendTeamUpdate(auth, payload) - items.push({ - teamUid: team.team_uid, - teamName: payload.team_name, - nodeId: targetNodeId, - status: UpdateTeamStatus.Updated, - }) - } catch (err) { - items.push({ - teamUid: team.team_uid, - teamName: team.name || '', - nodeId: team.node_id, - status: UpdateTeamStatus.Failed, - message: extractErrorMessage(err), - }) - } + const items: UpdateTeamItemResult[] = []; + for (const team of resolvedTeams) { + const targetNodeId = overrideNodeId ?? team.node_id; + const payload: TeamUpdateRequestPayload = { + team_uid: team.team_uid, + team_name: newName || team.name || "", + node_id: targetNodeId, + restrict_edit: restrictEditOverride ?? team.restrict_edit === true, + restrict_share: + restrictShareOverride ?? + (team.restrict_share === true || team.restrict_sharing === true), + restrict_view: restrictViewOverride ?? team.restrict_view === true, + }; + + try { + await sendTeamUpdate(auth, payload); + items.push({ + teamUid: team.team_uid, + teamName: payload.team_name, + nodeId: targetNodeId, + status: UpdateTeamStatus.Updated, + }); + } catch (err) { + items.push({ + teamUid: team.team_uid, + teamName: team.name || "", + nodeId: team.node_id, + status: UpdateTeamStatus.Failed, + message: extractErrorMessage(err), + }); } + } - return finalizeResult(items) + return finalizeResult(items); } -function resolveOptionalRestriction(input: TeamRestrictionInput): boolean | undefined { - if (input === undefined || input === null) return undefined - const lower = String(input).toLowerCase() - if (lower === TeamRestriction.On) return true - if (lower === TeamRestriction.Off) return false - return undefined +function resolveOptionalRestriction( + input: TeamRestrictionInput, +): boolean | undefined { + if (input === undefined || input === null) return undefined; + const lower = String(input).toLowerCase(); + if (lower === TeamRestriction.On) return true; + if (lower === TeamRestriction.Off) return false; + return undefined; } -async function sendTeamUpdate(auth: Auth, payload: TeamUpdateRequestPayload): Promise { - const command: RestCommand = { - baseRequest: { command: TEAM_UPDATE_COMMAND }, - request: payload, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `team_update failed for team_uid=${payload.team_uid}`, - response.result_code || ResultCodes.TEAM_UPDATE_FAILED - ) - } +async function sendTeamUpdate( + auth: Auth, + payload: TeamUpdateRequestPayload, +): Promise { + const command: RestCommand = { + baseRequest: { command: TEAM_UPDATE_COMMAND }, + request: payload, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `team_update failed for team_uid=${payload.team_uid}`, + response.result_code || ResultCodes.TEAM_UPDATE_FAILED, + ); + } } function finalizeResult(items: UpdateTeamItemResult[]): UpdateTeamResult { - const updated = items.filter((item) => item.status === UpdateTeamStatus.Updated).length - const failed = items.filter((item) => item.status === UpdateTeamStatus.Failed).length - return { - success: failed === 0 && updated > 0, - items, - updated, - failed, - } + const updated = items.filter( + (item) => item.status === UpdateTeamStatus.Updated, + ).length; + const failed = items.filter( + (item) => item.status === UpdateTeamStatus.Failed, + ).length; + return { + success: failed === 0 && updated > 0, + items, + updated, + failed, + }; } export type FormattedUpdateTeamRow = { - status: string - teamName: string - teamUid: string - nodeId: number - detail: string -} + status: string; + teamName: string; + teamUid: string; + nodeId: number; + detail: string; +}; export type FormattedUpdateTeamTable = { - headers: string[] - rows: string[][] - summary: string -} + headers: string[]; + rows: string[][]; + summary: string; +}; -export function formatUpdateTeamResult(result: UpdateTeamResult): FormattedUpdateTeamTable { - const rows = result.items.map((item, index) => [ - String(index + 1), - item.status, - item.teamName, - item.teamUid, - String(item.nodeId), - item.message || '', - ]) - - return { - headers: [...TEAM_TABLE_HEADERS], - rows, - summary: `Updated: ${result.updated} Failed: ${result.failed}`, - } +export function formatUpdateTeamResult( + result: UpdateTeamResult, +): FormattedUpdateTeamTable { + const rows = result.items.map((item, index) => [ + String(index + 1), + item.status, + item.teamName, + item.teamUid, + String(item.nodeId), + item.message || "", + ]); + + return { + headers: [...TEAM_TABLE_HEADERS], + rows, + summary: `Updated: ${result.updated} Failed: ${result.failed}`, + }; } -export function renderUpdateTeamAsciiTable(table: FormattedUpdateTeamTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell + ' '.repeat(Math.max(0, widths[columnIndex] - cell.length)) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [] - lines.push(formatRow(headers)) - lines.push(formatRow(widths.map((width) => '-'.repeat(width)))) - for (const row of rows) lines.push(formatRow(row)) - lines.push(table.summary) - return lines.join('\n') +export function renderUpdateTeamAsciiTable( + table: FormattedUpdateTeamTable, +): string { + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell + " ".repeat(Math.max(0, widths[columnIndex] - cell.length)); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = []; + lines.push(formatRow(headers)); + lines.push(formatRow(widths.map((width) => "-".repeat(width)))); + for (const row of rows) lines.push(formatRow(row)); + lines.push(table.summary); + return lines.join("\n"); } diff --git a/KeeperSdk/src/teams/viewTeam.ts b/KeeperSdk/src/teams/viewTeam.ts index bda5c4f2..f61669d9 100644 --- a/KeeperSdk/src/teams/viewTeam.ts +++ b/KeeperSdk/src/teams/viewTeam.ts @@ -1,248 +1,301 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseDisplayNames, - type EnterpriseNode, - type EnterpriseRoleTeamLink, - type EnterpriseTeamRecord, - type EnterpriseTeamUserLink, - type EnterpriseUser, -} from './enterpriseData' - -const NODE_PATH_SEPARATOR = '\\' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseRoleTeamLink, + type EnterpriseTeamRecord, + type EnterpriseTeamUserLink, + type EnterpriseUser, +} from "./enterpriseData"; + +const NODE_PATH_SEPARATOR = "\\"; const VIEW_TEAM_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Teams, - EnterpriseDataInclude.TeamUsers, - EnterpriseDataInclude.RoleTeams, - EnterpriseDataInclude.Roles, - EnterpriseDataInclude.Users, - EnterpriseDataInclude.Nodes, -] + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.TeamUsers, + EnterpriseDataInclude.RoleTeams, + EnterpriseDataInclude.Roles, + EnterpriseDataInclude.Users, + EnterpriseDataInclude.Nodes, +]; export type TeamRoleInfo = { - role_id: number - role_name: string -} + role_id: number; + role_name: string; +}; export type TeamUserInfo = { - enterprise_user_id: number - username: string -} + enterprise_user_id: number; + username: string; +}; export type TeamView = { - team_uid: string - team_name: string - node_id: number - node_name: string - restrict_view: boolean - restrict_edit: boolean - restrict_share: boolean - team_roles?: TeamRoleInfo[] - team_users?: TeamUserInfo[] -} + team_uid: string; + team_name: string; + node_id: number; + node_name: string; + restrict_view: boolean; + restrict_edit: boolean; + restrict_share: boolean; + team_roles?: TeamRoleInfo[]; + team_users?: TeamUserInfo[]; +}; export type FormatTeamViewOptions = { - verbose?: boolean -} + verbose?: boolean; +}; export type TeamViewTableRow = { - label: string - value: string | string[] - id?: number | number[] -} + label: string; + value: string | string[]; + id?: number | number[]; +}; export type FormattedTeamViewTable = { - rows: TeamViewTableRow[] - hasIdColumn: boolean -} + rows: TeamViewTableRow[]; + hasIdColumn: boolean; +}; -export async function viewTeam(auth: Auth, identifier: string): Promise { - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(VIEW_TEAM_INCLUDES), - enterpriseData.getDisplayNames(), - ]) - - const team = resolveTeam(response.teams || [], identifier) - const nodePath = resolveNodePath(response.nodes || [], displayNames, team.node_id) - const teamUsers = buildTeamUsers(response.users || [], response.team_users || [], team.team_uid) - const teamRoles = buildTeamRoles(response.role_teams || [], displayNames.roles, team.team_uid) - - const view: TeamView = { - team_uid: team.team_uid, - team_name: team.name, - node_id: team.node_id, - node_name: nodePath, - restrict_view: team.restrict_view === true, - restrict_edit: team.restrict_edit === true, - restrict_share: team.restrict_share === true || team.restrict_sharing === true, - } - if (teamRoles.length > 0) view.team_roles = teamRoles - if (teamUsers.length > 0) view.team_users = teamUsers - return view +export async function viewTeam( + auth: Auth, + identifier: string, +): Promise { + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(VIEW_TEAM_INCLUDES), + enterpriseData.getDisplayNames(), + ]); + + const team = resolveTeam(response.teams || [], identifier); + const nodePath = resolveNodePath( + response.nodes || [], + displayNames, + team.node_id, + ); + const teamUsers = buildTeamUsers( + response.users || [], + response.team_users || [], + team.team_uid, + ); + const teamRoles = buildTeamRoles( + response.role_teams || [], + displayNames.roles, + team.team_uid, + ); + + const view: TeamView = { + team_uid: team.team_uid, + team_name: team.name, + node_id: team.node_id, + node_name: nodePath, + restrict_view: team.restrict_view === true, + restrict_edit: team.restrict_edit === true, + restrict_share: + team.restrict_share === true || team.restrict_sharing === true, + }; + if (teamRoles.length > 0) view.team_roles = teamRoles; + if (teamUsers.length > 0) view.team_users = teamUsers; + return view; } -export function formatTeamView(view: TeamView, options: FormatTeamViewOptions = {}): FormattedTeamViewTable { - const verbose = options.verbose === true - const rows: TeamViewTableRow[] = [ - { label: 'Team UID', value: view.team_uid }, - { label: 'Team Name', value: view.team_name }, - { - label: 'Node Name', - value: view.node_name, - id: verbose ? view.node_id : undefined, - }, - { label: 'Restrict Edit', value: boolText(view.restrict_edit) }, - { label: 'Restrict Share', value: boolText(view.restrict_share) }, - { label: 'Restrict View', value: boolText(view.restrict_view) }, - ] - - if (view.team_roles && view.team_roles.length > 0) { - rows.push({ - label: 'Role(s)', - value: view.team_roles.map((role) => role.role_name), - id: verbose ? view.team_roles.map((role) => role.role_id) : undefined, - }) - } +export function formatTeamView( + view: TeamView, + options: FormatTeamViewOptions = {}, +): FormattedTeamViewTable { + const verbose = options.verbose === true; + const rows: TeamViewTableRow[] = [ + { label: "Team UID", value: view.team_uid }, + { label: "Team Name", value: view.team_name }, + { + label: "Node Name", + value: view.node_name, + id: verbose ? view.node_id : undefined, + }, + { label: "Restrict Edit", value: boolText(view.restrict_edit) }, + { label: "Restrict Share", value: boolText(view.restrict_share) }, + { label: "Restrict View", value: boolText(view.restrict_view) }, + ]; - if (view.team_users && view.team_users.length > 0) { - rows.push({ - label: 'User(s)', - value: view.team_users.map((user) => user.username), - id: verbose ? view.team_users.map((user) => user.enterprise_user_id) : undefined, - }) - } + if (view.team_roles && view.team_roles.length > 0) { + rows.push({ + label: "Role(s)", + value: view.team_roles.map((role) => role.role_name), + id: verbose ? view.team_roles.map((role) => role.role_id) : undefined, + }); + } - return { rows, hasIdColumn: verbose } + if (view.team_users && view.team_users.length > 0) { + rows.push({ + label: "User(s)", + value: view.team_users.map((user) => user.username), + id: verbose + ? view.team_users.map((user) => user.enterprise_user_id) + : undefined, + }); + } + + return { rows, hasIdColumn: verbose }; } export function teamViewTable(table: FormattedTeamViewTable): string { - const labelWidth = table.rows.reduce((max, row) => Math.max(max, row.label.length), 0) - const expandedRows = table.rows.map((row) => ({ - label: row.label, - valueLines: asLines(row.value), - idLines: table.hasIdColumn ? asIdLines(row.id) : [], - })) - - const valueWidth = expandedRows.reduce( - (max, row) => Math.max(max, ...row.valueLines.map((line) => line.length)), - 0 - ) - const idWidth = table.hasIdColumn - ? expandedRows.reduce((max, row) => Math.max(max, ...row.idLines.map((line) => line.length)), 0) - : 0 - - const padRight = (text: string, width: number): string => - text + ' '.repeat(Math.max(0, width - text.length)) - const padLeft = (text: string, width: number): string => - ' '.repeat(Math.max(0, width - text.length)) + text - - const lines: string[] = [] - for (const row of expandedRows) { - const lineCount = Math.max(row.valueLines.length, table.hasIdColumn ? row.idLines.length : 0, 1) - for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) { - const isFirstLine = lineIndex === 0 - const labelCell = padLeft(isFirstLine ? row.label : '', labelWidth) - const valueCell = padRight(row.valueLines[lineIndex] ?? '', valueWidth) - const cells: string[] = [labelCell, valueCell] - if (table.hasIdColumn) { - const idCell = padRight(row.idLines[lineIndex] ?? '', idWidth) - cells.push(idCell) - } - lines.push(cells.join(' ').trimEnd()) - } + const labelWidth = table.rows.reduce( + (max, row) => Math.max(max, row.label.length), + 0, + ); + const expandedRows = table.rows.map((row) => ({ + label: row.label, + valueLines: asLines(row.value), + idLines: table.hasIdColumn ? asIdLines(row.id) : [], + })); + + const valueWidth = expandedRows.reduce( + (max, row) => Math.max(max, ...row.valueLines.map((line) => line.length)), + 0, + ); + const idWidth = table.hasIdColumn + ? expandedRows.reduce( + (max, row) => Math.max(max, ...row.idLines.map((line) => line.length)), + 0, + ) + : 0; + + const padRight = (text: string, width: number): string => + text + " ".repeat(Math.max(0, width - text.length)); + const padLeft = (text: string, width: number): string => + " ".repeat(Math.max(0, width - text.length)) + text; + + const lines: string[] = []; + for (const row of expandedRows) { + const lineCount = Math.max( + row.valueLines.length, + table.hasIdColumn ? row.idLines.length : 0, + 1, + ); + for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) { + const isFirstLine = lineIndex === 0; + const labelCell = padLeft(isFirstLine ? row.label : "", labelWidth); + const valueCell = padRight(row.valueLines[lineIndex] ?? "", valueWidth); + const cells: string[] = [labelCell, valueCell]; + if (table.hasIdColumn) { + const idCell = padRight(row.idLines[lineIndex] ?? "", idWidth); + cells.push(idCell); + } + lines.push(cells.join(" ").trimEnd()); } - return lines.join('\n') + } + return lines.join("\n"); } -function resolveTeam(teams: EnterpriseTeamRecord[], identifier: string): EnterpriseTeamRecord { - const trimmed = (identifier ?? '').trim() - if (!trimmed) { - throw new KeeperSdkError('Team name or UID is required.', ResultCodes.TEAM_REQUIRED) - } +function resolveTeam( + teams: EnterpriseTeamRecord[], + identifier: string, +): EnterpriseTeamRecord { + const trimmed = (identifier ?? "").trim(); + if (!trimmed) { + throw new KeeperSdkError( + "Team name or UID is required.", + ResultCodes.TEAM_REQUIRED, + ); + } - const uidMatch = teams.find((team) => team.team_uid === trimmed) - if (uidMatch) return uidMatch - - const lowered = trimmed.toLowerCase() - const nameMatches = teams.filter((team) => (team.name || '').trim().toLowerCase() === lowered) - if (nameMatches.length === 1) return nameMatches[0] - if (nameMatches.length > 1) { - throw new KeeperSdkError( - `Multiple teams match name "${trimmed}". Specify the team UID instead.`, - ResultCodes.MULTIPLE_TEAM_MATCHES - ) - } - throw new KeeperSdkError(`Team "${trimmed}" does not exist.`, ResultCodes.TEAM_NOT_FOUND) + const uidMatch = teams.find((team) => team.team_uid === trimmed); + if (uidMatch) return uidMatch; + + const lowered = trimmed.toLowerCase(); + const nameMatches = teams.filter( + (team) => (team.name || "").trim().toLowerCase() === lowered, + ); + if (nameMatches.length === 1) return nameMatches[0]; + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Multiple teams match name "${trimmed}". Specify the team UID instead.`, + ResultCodes.MULTIPLE_TEAM_MATCHES, + ); + } + throw new KeeperSdkError( + `Team "${trimmed}" does not exist.`, + ResultCodes.TEAM_NOT_FOUND, + ); } function resolveNodePath( - nodes: EnterpriseNode[], - displayNames: EnterpriseDisplayNames, - nodeId: number + nodes: EnterpriseNode[], + displayNames: EnterpriseDisplayNames, + nodeId: number, ): string { - if (displayNames.nodes.size > 0) { - for (const node of nodes) { - const display = displayNames.nodes.get(node.node_id) - if (display) node.displayName = display - } + if (displayNames.nodes.size > 0) { + for (const node of nodes) { + const display = displayNames.nodes.get(node.node_id); + if (display) node.displayName = display; } - return EnterpriseDataManager.getNodePath(nodes, nodeId, { omitRoot: false, separator: NODE_PATH_SEPARATOR }) + } + return EnterpriseDataManager.getNodePath(nodes, nodeId, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }); } function buildTeamUsers( - users: EnterpriseUser[], - teamUserLinks: EnterpriseTeamUserLink[], - teamUid: string + users: EnterpriseUser[], + teamUserLinks: EnterpriseTeamUserLink[], + teamUid: string, ): TeamUserInfo[] { - const userById = new Map() - for (const user of users) userById.set(user.enterprise_user_id, user) + const userById = new Map(); + for (const user of users) userById.set(user.enterprise_user_id, user); - const memberIds = new Set() - for (const link of teamUserLinks) { - if (link.team_uid === teamUid) memberIds.add(link.enterprise_user_id) - } + const memberIds = new Set(); + for (const link of teamUserLinks) { + if (link.team_uid === teamUid) memberIds.add(link.enterprise_user_id); + } - const result: TeamUserInfo[] = [] - for (const id of memberIds) { - const user = userById.get(id) - if (user && user.username) result.push({ enterprise_user_id: id, username: user.username }) - } - result.sort((a, b) => a.username.localeCompare(b.username, undefined, { sensitivity: 'base' })) - return result + const result: TeamUserInfo[] = []; + for (const id of memberIds) { + const user = userById.get(id); + if (user && user.username) + result.push({ enterprise_user_id: id, username: user.username }); + } + result.sort((a, b) => + a.username.localeCompare(b.username, undefined, { sensitivity: "base" }), + ); + return result; } function buildTeamRoles( - roleTeamLinks: EnterpriseRoleTeamLink[], - decryptedRoleNames: Map, - teamUid: string + roleTeamLinks: EnterpriseRoleTeamLink[], + decryptedRoleNames: Map, + teamUid: string, ): TeamRoleInfo[] { - const roleIds = new Set() - for (const link of roleTeamLinks) { - if (link.team_uid === teamUid) roleIds.add(link.role_id) - } + const roleIds = new Set(); + for (const link of roleTeamLinks) { + if (link.team_uid === teamUid) roleIds.add(link.role_id); + } - const result: TeamRoleInfo[] = [] - for (const id of roleIds) { - result.push({ role_id: id, role_name: decryptedRoleNames.get(id) || String(id) }) - } - result.sort((a, b) => a.role_name.localeCompare(b.role_name, undefined, { sensitivity: 'base' })) - return result + const result: TeamRoleInfo[] = []; + for (const id of roleIds) { + result.push({ + role_id: id, + role_name: decryptedRoleNames.get(id) || String(id), + }); + } + result.sort((a, b) => + a.role_name.localeCompare(b.role_name, undefined, { sensitivity: "base" }), + ); + return result; } function boolText(value: boolean): string { - return value ? 'True' : 'False' + return value ? "True" : "False"; } function asLines(value: string | string[]): string[] { - if (Array.isArray(value)) return value.length > 0 ? value : [''] - return [value] + if (Array.isArray(value)) return value.length > 0 ? value : [""]; + return [value]; } function asIdLines(id: number | number[] | undefined): string[] { - if (id == null) return [] - if (Array.isArray(id)) return id.length > 0 ? id.map(String) : [''] - return [String(id)] + if (id == null) return []; + if (Array.isArray(id)) return id.length > 0 ? id.map(String) : [""]; + return [String(id)]; } diff --git a/KeeperSdk/src/users/UserManager.ts b/KeeperSdk/src/users/UserManager.ts index 6db91cc2..aee8b44a 100644 --- a/KeeperSdk/src/users/UserManager.ts +++ b/KeeperSdk/src/users/UserManager.ts @@ -1,149 +1,201 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { KeeperSdkError, ResultCodes } from '../utils' -import { addUsers, formatAddUserResult, renderAddUserAsciiTable } from './addUser' -import { updateUsers, formatUpdateUserResult, renderUpdateUserAsciiTable } from './updateUser' -import { deleteUsers, formatDeleteUserResult, renderDeleteUserAsciiTable } from './deleteUser' -import { actionUsers, formatUserActionResult, renderUserActionAsciiTable } from './actionUser' -import { aliasUser } from './aliasUser' -import { addUsersToTeams, removeUsersFromTeams, formatTeamUserResult, renderTeamUserAsciiTable } from './teamUser' -import { listUsers, formatUsersTable, renderUsersAsciiTable } from './listUsers' -import { viewUser, formatUserView, userViewTable } from './viewUser' +import type { Auth } from "@keeper-security/keeperapi"; +import { KeeperSdkError, ResultCodes } from "../utils"; +import { + addUsers, + formatAddUserResult, + renderAddUserAsciiTable, +} from "./addUser"; +import { + updateUsers, + formatUpdateUserResult, + renderUpdateUserAsciiTable, +} from "./updateUser"; +import { + deleteUsers, + formatDeleteUserResult, + renderDeleteUserAsciiTable, +} from "./deleteUser"; +import { + actionUsers, + formatUserActionResult, + renderUserActionAsciiTable, +} from "./actionUser"; +import { aliasUser } from "./aliasUser"; +import { + addUsersToTeams, + removeUsersFromTeams, + formatTeamUserResult, + renderTeamUserAsciiTable, +} from "./teamUser"; +import { + listUsers, + formatUsersTable, + renderUsersAsciiTable, +} from "./listUsers"; +import { viewUser, formatUserView, userViewTable } from "./viewUser"; import type { - UserColumnInput, - ListUsersOptions, - ListUserRow, - FormattedUsersTable, - FormatUsersTableOptions, - UserView, - FormatUserViewOptions, - FormattedUserViewTable, - AddUserInput, - AddUserResult, - FormatAddUserResultOptions, - FormattedAddUserTable, - UpdateUserInput, - UpdateUserResult, - FormattedUpdateUserTable, - DeleteUserInput, - DeleteUserResult, - FormattedDeleteUserTable, - UserActionInput, - UserActionResult, - FormattedUserActionTable, - AliasUserInput, - AliasUserResult, - AddUsersToTeamsInput, - RemoveUsersFromTeamsInput, - TeamUserResult, - FormattedTeamUserTable, -} from './userTypes' - -export type AuthProvider = () => Auth + UserColumnInput, + ListUsersOptions, + ListUserRow, + FormattedUsersTable, + FormatUsersTableOptions, + UserView, + FormatUserViewOptions, + FormattedUserViewTable, + AddUserInput, + AddUserResult, + FormatAddUserResultOptions, + FormattedAddUserTable, + UpdateUserInput, + UpdateUserResult, + FormattedUpdateUserTable, + DeleteUserInput, + DeleteUserResult, + FormattedDeleteUserTable, + UserActionInput, + UserActionResult, + FormattedUserActionTable, + AliasUserInput, + AliasUserResult, + AddUsersToTeamsInput, + RemoveUsersFromTeamsInput, + TeamUserResult, + FormattedTeamUserTable, +} from "./userTypes"; + +export type AuthProvider = () => Auth; export class UserManager { - private readonly authProvider: AuthProvider - - constructor(authProvider: AuthProvider) { - this.authProvider = authProvider - } - - public async listUsers(options: ListUsersOptions = {}): Promise { - return listUsers(this.requireAuth(), options) - } - - public formatUsersTable(rows: ListUserRow[], options: FormatUsersTableOptions = {}): FormattedUsersTable { - return formatUsersTable(rows, options) - } - - public renderUsersAsciiTable(table: FormattedUsersTable, options: { minColWidth?: number } = {}): string { - return renderUsersAsciiTable(table, options) - } - - public async viewUser(identifier: string): Promise { - return viewUser(this.requireAuth(), identifier) - } - - public formatUserView(view: UserView, options: FormatUserViewOptions = {}): FormattedUserViewTable { - return formatUserView(view, options) - } - - public userViewTable(table: FormattedUserViewTable): string { - return userViewTable(table) - } - - public async addUsers(input: AddUserInput): Promise { - return addUsers(this.requireAuth(), input) - } - - public formatAddUserResult(result: AddUserResult, options: FormatAddUserResultOptions = {}): FormattedAddUserTable { - return formatAddUserResult(result, options) - } - - public renderAddUserAsciiTable(table: FormattedAddUserTable): string { - return renderAddUserAsciiTable(table) - } - - public async updateUsers(input: UpdateUserInput): Promise { - return updateUsers(this.requireAuth(), input) - } - - public formatUpdateUserResult(result: UpdateUserResult): FormattedUpdateUserTable { - return formatUpdateUserResult(result) - } - - public renderUpdateUserAsciiTable(table: FormattedUpdateUserTable): string { - return renderUpdateUserAsciiTable(table) - } - - public async deleteUsers(input: DeleteUserInput): Promise { - return deleteUsers(this.requireAuth(), input) - } - - public formatDeleteUserResult(result: DeleteUserResult): FormattedDeleteUserTable { - return formatDeleteUserResult(result) - } - - public renderDeleteUserAsciiTable(table: FormattedDeleteUserTable): string { - return renderDeleteUserAsciiTable(table) - } - - public async actionUsers(input: UserActionInput): Promise { - return actionUsers(this.requireAuth(), input) - } - - public formatUserActionResult(result: UserActionResult): FormattedUserActionTable { - return formatUserActionResult(result) - } - - public renderUserActionAsciiTable(table: FormattedUserActionTable): string { - return renderUserActionAsciiTable(table) - } - - public async aliasUser(input: AliasUserInput): Promise { - return aliasUser(this.requireAuth(), input) - } - - public async addUsersToTeams(input: AddUsersToTeamsInput): Promise { - return addUsersToTeams(this.requireAuth(), input) - } - - public async removeUsersFromTeams(input: RemoveUsersFromTeamsInput): Promise { - return removeUsersFromTeams(this.requireAuth(), input) - } - - public formatTeamUserResult(result: TeamUserResult): FormattedTeamUserTable { - return formatTeamUserResult(result) - } - - public renderTeamUserAsciiTable(table: FormattedTeamUserTable): string { - return renderTeamUserAsciiTable(table) - } - - private requireAuth(): Auth { - const auth = this.authProvider() - if (!auth) { - throw new KeeperSdkError('You are not logged in. Please log in first.', ResultCodes.NOT_LOGGED_IN) - } - return auth - } -} \ No newline at end of file + private readonly authProvider: AuthProvider; + + constructor(authProvider: AuthProvider) { + this.authProvider = authProvider; + } + + public async listUsers( + options: ListUsersOptions = {}, + ): Promise { + return listUsers(this.requireAuth(), options); + } + + public formatUsersTable( + rows: ListUserRow[], + options: FormatUsersTableOptions = {}, + ): FormattedUsersTable { + return formatUsersTable(rows, options); + } + + public renderUsersAsciiTable( + table: FormattedUsersTable, + options: { minColWidth?: number } = {}, + ): string { + return renderUsersAsciiTable(table, options); + } + + public async viewUser(identifier: string): Promise { + return viewUser(this.requireAuth(), identifier); + } + + public formatUserView( + view: UserView, + options: FormatUserViewOptions = {}, + ): FormattedUserViewTable { + return formatUserView(view, options); + } + + public userViewTable(table: FormattedUserViewTable): string { + return userViewTable(table); + } + + public async addUsers(input: AddUserInput): Promise { + return addUsers(this.requireAuth(), input); + } + + public formatAddUserResult( + result: AddUserResult, + options: FormatAddUserResultOptions = {}, + ): FormattedAddUserTable { + return formatAddUserResult(result, options); + } + + public renderAddUserAsciiTable(table: FormattedAddUserTable): string { + return renderAddUserAsciiTable(table); + } + + public async updateUsers(input: UpdateUserInput): Promise { + return updateUsers(this.requireAuth(), input); + } + + public formatUpdateUserResult( + result: UpdateUserResult, + ): FormattedUpdateUserTable { + return formatUpdateUserResult(result); + } + + public renderUpdateUserAsciiTable(table: FormattedUpdateUserTable): string { + return renderUpdateUserAsciiTable(table); + } + + public async deleteUsers(input: DeleteUserInput): Promise { + return deleteUsers(this.requireAuth(), input); + } + + public formatDeleteUserResult( + result: DeleteUserResult, + ): FormattedDeleteUserTable { + return formatDeleteUserResult(result); + } + + public renderDeleteUserAsciiTable(table: FormattedDeleteUserTable): string { + return renderDeleteUserAsciiTable(table); + } + + public async actionUsers(input: UserActionInput): Promise { + return actionUsers(this.requireAuth(), input); + } + + public formatUserActionResult( + result: UserActionResult, + ): FormattedUserActionTable { + return formatUserActionResult(result); + } + + public renderUserActionAsciiTable(table: FormattedUserActionTable): string { + return renderUserActionAsciiTable(table); + } + + public async aliasUser(input: AliasUserInput): Promise { + return aliasUser(this.requireAuth(), input); + } + + public async addUsersToTeams( + input: AddUsersToTeamsInput, + ): Promise { + return addUsersToTeams(this.requireAuth(), input); + } + + public async removeUsersFromTeams( + input: RemoveUsersFromTeamsInput, + ): Promise { + return removeUsersFromTeams(this.requireAuth(), input); + } + + public formatTeamUserResult(result: TeamUserResult): FormattedTeamUserTable { + return formatTeamUserResult(result); + } + + public renderTeamUserAsciiTable(table: FormattedTeamUserTable): string { + return renderTeamUserAsciiTable(table); + } + + private requireAuth(): Auth { + const auth = this.authProvider(); + if (!auth) { + throw new KeeperSdkError( + "You are not logged in. Please log in first.", + ResultCodes.NOT_LOGGED_IN, + ); + } + return auth; + } +} diff --git a/KeeperSdk/src/users/actionUser.ts b/KeeperSdk/src/users/actionUser.ts index bacc22ae..15818043 100644 --- a/KeeperSdk/src/users/actionUser.ts +++ b/KeeperSdk/src/users/actionUser.ts @@ -1,351 +1,404 @@ import { - Enterprise, - enterpriseUsersLockMessage, - type Auth, - type KeeperResponse, - type RestCommand, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, isNumber, KeeperSdkError, logger, ResultCodes } from '../utils' + Enterprise, + enterpriseUsersLockMessage, + type Auth, + type KeeperResponse, + type RestCommand, +} from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseUser, -} from '../teams/enterpriseData' + extractErrorMessage, + isNumber, + KeeperSdkError, + logger, + ResultCodes, +} from "../utils"; import { - EnterpriseUserStatus, - normalizeEmailInputs, - UserAction, - UserActionStatus, - UserActionSkipReason, - type UserActionInput, - type UserActionItemResult, - type UserActionResult, - type FormattedUserActionTable, -} from './userTypes' - -export { UserAction, UserActionStatus, UserActionSkipReason } -export type { UserActionInput, UserActionItemResult, UserActionResult, FormattedUserActionTable } - -const EXPIRE_PASSWORD_COMMAND = 'set_master_password_expire' -const ALL_USERS_SENTINEL = '@all' -const ACTIONS_NOT_SUPPORTING_ALL = new Set([UserAction.Lock, UserAction.ExpirePassword]) -const LOCK_UNLOCK_BATCH_SIZE = 1000 -const SELF_ACTION_MESSAGE = 'This operation cannot be done on yourself.' - -const ACTION_USER_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Users] - -const USER_ACTION_TABLE_HEADERS = ['#', 'Status', 'Email', 'User ID', 'Detail'] + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseUser, +} from "../teams/enterpriseData"; +import { + EnterpriseUserStatus, + normalizeEmailInputs, + UserAction, + UserActionStatus, + UserActionSkipReason, + type UserActionInput, + type UserActionItemResult, + type UserActionResult, + type FormattedUserActionTable, +} from "./userTypes"; + +export { UserAction, UserActionStatus, UserActionSkipReason }; +export type { + UserActionInput, + UserActionItemResult, + UserActionResult, + FormattedUserActionTable, +}; + +const EXPIRE_PASSWORD_COMMAND = "set_master_password_expire"; +const ALL_USERS_SENTINEL = "@all"; +const ACTIONS_NOT_SUPPORTING_ALL = new Set([ + UserAction.Lock, + UserAction.ExpirePassword, +]); +const LOCK_UNLOCK_BATCH_SIZE = 1000; +const SELF_ACTION_MESSAGE = "This operation cannot be done on yourself."; + +const ACTION_USER_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Users, +]; + +const USER_ACTION_TABLE_HEADERS = ["#", "Status", "Email", "User ID", "Detail"]; type ExpirePasswordPayload = { - email: string -} + email: string; +}; type ActionUserTarget = - | { kind: 'user'; user: EnterpriseUser } - | { kind: 'not_found'; identifier: string } - -export async function actionUsers(auth: Auth, input: UserActionInput): Promise { - const identifiers = normalizeEmailInputs(input.emails) - - if (identifiers.length === 0) { - throw new KeeperSdkError('No users provided.', ResultCodes.NO_USERS_TO_ACTION) + | { kind: "user"; user: EnterpriseUser } + | { kind: "not_found"; identifier: string }; + +export async function actionUsers( + auth: Auth, + input: UserActionInput, +): Promise { + const identifiers = normalizeEmailInputs(input.emails); + + if (identifiers.length === 0) { + throw new KeeperSdkError( + "No users provided.", + ResultCodes.NO_USERS_TO_ACTION, + ); + } + + const isAll = identifiers.includes(ALL_USERS_SENTINEL); + + if (isAll && ACTIONS_NOT_SUPPORTING_ALL.has(input.action)) { + throw new KeeperSdkError( + `The '${input.action}' action does not support @all.`, + ResultCodes.USER_ACTION_ALL_NOT_SUPPORTED, + ); + } + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(ACTION_USER_INCLUDES); + const allUsers = response.users || []; + + const targets: ActionUserTarget[] = isAll + ? allUsers.map((user) => ({ kind: "user" as const, user })) + : resolveActionUserTargets(allUsers, identifiers); + const callerEnterpriseUserId = resolveCallerEnterpriseUserId(auth, allUsers); + + const items: UserActionItemResult[] = []; + const batchTargets = new Map(); + + for (const target of targets) { + if (target.kind === "not_found") { + items.push({ + username: target.identifier, + status: UserActionStatus.Failed, + message: `User "${target.identifier}" does not exist.`, + }); + continue; } - const isAll = identifiers.includes(ALL_USERS_SENTINEL) - - if (isAll && ACTIONS_NOT_SUPPORTING_ALL.has(input.action)) { - throw new KeeperSdkError( - `The '${input.action}' action does not support @all.`, - ResultCodes.USER_ACTION_ALL_NOT_SUPPORTED - ) + const user = target.user; + const item: UserActionItemResult = { + username: user.username, + enterpriseUserId: user.enterprise_user_id, + status: UserActionStatus.Failed, + }; + + if (user.status !== EnterpriseUserStatus.Active) { + const masked = maskEmail(user.username); + logger.warn(`User "${masked}" is not active and will be skipped.`); + item.status = UserActionStatus.Skipped; + item.skipReason = UserActionSkipReason.Inactive; + items.push(item); + continue; } - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(ACTION_USER_INCLUDES) - const allUsers = response.users || [] - - const targets: ActionUserTarget[] = isAll - ? allUsers.map((user) => ({ kind: 'user' as const, user })) - : resolveActionUserTargets(allUsers, identifiers) - const callerEnterpriseUserId = resolveCallerEnterpriseUserId(auth, allUsers) - - const items: UserActionItemResult[] = [] - const batchTargets = new Map() - - for (const target of targets) { - if (target.kind === 'not_found') { - items.push({ - username: target.identifier, - status: UserActionStatus.Failed, - message: `User "${target.identifier}" does not exist.`, - }) - continue - } - - const user = target.user - const item: UserActionItemResult = { - username: user.username, - enterpriseUserId: user.enterprise_user_id, - status: UserActionStatus.Failed, - } - - if (user.status !== EnterpriseUserStatus.Active) { - const masked = maskEmail(user.username) - logger.warn(`User "${masked}" is not active and will be skipped.`) - item.status = UserActionStatus.Skipped - item.skipReason = UserActionSkipReason.Inactive - items.push(item) - continue - } - - items.push(item) - - if (input.action === UserAction.Lock || input.action === UserAction.Unlock) { - if (callerEnterpriseUserId !== null && user.enterprise_user_id === callerEnterpriseUserId) { - logger.warn(`User "${maskEmail(user.username)}" is the logged-in user and will be skipped for lock/unlock.`) - item.status = UserActionStatus.Failed - item.message = SELF_ACTION_MESSAGE - continue - } - batchTargets.set(user.enterprise_user_id, item) - continue - } - - try { - await sendExpirePassword(auth, user.username) - item.status = UserActionStatus.Success - } catch (err) { - item.message = extractErrorMessage(err) - } + items.push(item); + + if ( + input.action === UserAction.Lock || + input.action === UserAction.Unlock + ) { + if ( + callerEnterpriseUserId !== null && + user.enterprise_user_id === callerEnterpriseUserId + ) { + logger.warn( + `User "${maskEmail(user.username)}" is the logged-in user and will be skipped for lock/unlock.`, + ); + item.status = UserActionStatus.Failed; + item.message = SELF_ACTION_MESSAGE; + continue; + } + batchTargets.set(user.enterprise_user_id, item); + continue; } - if (input.action === UserAction.Lock || input.action === UserAction.Unlock) { - await sendBatchLockUnlock(auth, input.action, batchTargets) + try { + await sendExpirePassword(auth, user.username); + item.status = UserActionStatus.Success; + } catch (err) { + item.message = extractErrorMessage(err); } + } - return finalizeResult(items) + if (input.action === UserAction.Lock || input.action === UserAction.Unlock) { + await sendBatchLockUnlock(auth, input.action, batchTargets); + } + + return finalizeResult(items); } async function sendBatchLockUnlock( - auth: Auth, - action: UserAction.Lock | UserAction.Unlock, - batchTargets: Map + auth: Auth, + action: UserAction.Lock | UserAction.Unlock, + batchTargets: Map, ): Promise { - if (batchTargets.size === 0) { - return + if (batchTargets.size === 0) { + return; + } + + const enterpriseUserIds = [...batchTargets.keys()]; + + for (const chunk of chunkArray(enterpriseUserIds, LOCK_UNLOCK_BATCH_SIZE)) { + const response = await auth.executeRest( + enterpriseUsersLockMessage({ + lockEnterpriseUserIds: action === UserAction.Lock ? chunk : [], + disableEnterpriseUserIds: [], + unlockEnterpriseUserIds: action === UserAction.Unlock ? chunk : [], + deleteIfPending: false, + }), + ); + + const seen = new Set(); + for (const lockResponse of response.response || []) { + const enterpriseUserId = toEnterpriseUserId( + lockResponse.enterpriseUserId, + ); + if (enterpriseUserId === null) { + continue; + } + + seen.add(enterpriseUserId); + const item = batchTargets.get(enterpriseUserId); + if (!item) { + continue; + } + + applyLockUserResponse(item, lockResponse, action); } - const enterpriseUserIds = [...batchTargets.keys()] - - for (const chunk of chunkArray(enterpriseUserIds, LOCK_UNLOCK_BATCH_SIZE)) { - const response = await auth.executeRest( - enterpriseUsersLockMessage({ - lockEnterpriseUserIds: action === UserAction.Lock ? chunk : [], - disableEnterpriseUserIds: [], - unlockEnterpriseUserIds: action === UserAction.Unlock ? chunk : [], - deleteIfPending: false, - }) - ) - - const seen = new Set() - for (const lockResponse of response.response || []) { - const enterpriseUserId = toEnterpriseUserId(lockResponse.enterpriseUserId) - if (enterpriseUserId === null) { - continue - } - - seen.add(enterpriseUserId) - const item = batchTargets.get(enterpriseUserId) - if (!item) { - continue - } - - applyLockUserResponse(item, lockResponse, action) - } - - for (const enterpriseUserId of chunk) { - if (seen.has(enterpriseUserId)) { - continue - } - const item = batchTargets.get(enterpriseUserId) - if (!item) { - continue - } - item.status = UserActionStatus.Failed - item.message = 'No response received for user.' - } + for (const enterpriseUserId of chunk) { + if (seen.has(enterpriseUserId)) { + continue; + } + const item = batchTargets.get(enterpriseUserId); + if (!item) { + continue; + } + item.status = UserActionStatus.Failed; + item.message = "No response received for user."; } + } } function applyLockUserResponse( - item: UserActionItemResult, - lockResponse: Enterprise.ILockUserResponse, - action: UserAction.Lock | UserAction.Unlock + item: UserActionItemResult, + lockResponse: Enterprise.ILockUserResponse, + action: UserAction.Lock | UserAction.Unlock, ): void { - const status = lockResponse.status ?? Enterprise.UserLockStatus.UNKNOWN_LOCK_STATUS - const errorMessage = lockResponse.errorMessage || '' - - switch (status) { - case Enterprise.UserLockStatus.LOCKED: - if (action === UserAction.Lock) { - item.status = UserActionStatus.Success - } else { - item.status = UserActionStatus.Failed - item.message = errorMessage || 'User is locked.' - } - break - case Enterprise.UserLockStatus.UNLOCKED: - if (action === UserAction.Unlock) { - item.status = UserActionStatus.Success - } else { - item.status = UserActionStatus.Failed - item.message = errorMessage || 'User is unlocked.' - } - break - case Enterprise.UserLockStatus.CANT_BE_PENDING: - item.status = UserActionStatus.Skipped - item.skipReason = UserActionSkipReason.Pending - item.message = errorMessage || 'Pending user was not modified.' - break - case Enterprise.UserLockStatus.DELETED: - item.status = UserActionStatus.Success - item.message = errorMessage || 'Pending user was deleted.' - break - case Enterprise.UserLockStatus.DISABLED: - item.status = UserActionStatus.Failed - item.message = errorMessage || 'User was disabled.' - break - default: - item.status = UserActionStatus.Failed - item.message = errorMessage || 'Lock operation failed.' - } + const status = + lockResponse.status ?? Enterprise.UserLockStatus.UNKNOWN_LOCK_STATUS; + const errorMessage = lockResponse.errorMessage || ""; + + switch (status) { + case Enterprise.UserLockStatus.LOCKED: + if (action === UserAction.Lock) { + item.status = UserActionStatus.Success; + } else { + item.status = UserActionStatus.Failed; + item.message = errorMessage || "User is locked."; + } + break; + case Enterprise.UserLockStatus.UNLOCKED: + if (action === UserAction.Unlock) { + item.status = UserActionStatus.Success; + } else { + item.status = UserActionStatus.Failed; + item.message = errorMessage || "User is unlocked."; + } + break; + case Enterprise.UserLockStatus.CANT_BE_PENDING: + item.status = UserActionStatus.Skipped; + item.skipReason = UserActionSkipReason.Pending; + item.message = errorMessage || "Pending user was not modified."; + break; + case Enterprise.UserLockStatus.DELETED: + item.status = UserActionStatus.Success; + item.message = errorMessage || "Pending user was deleted."; + break; + case Enterprise.UserLockStatus.DISABLED: + item.status = UserActionStatus.Failed; + item.message = errorMessage || "User was disabled."; + break; + default: + item.status = UserActionStatus.Failed; + item.message = errorMessage || "Lock operation failed."; + } } async function sendExpirePassword(auth: Auth, email: string): Promise { - const command: RestCommand = { - baseRequest: { command: EXPIRE_PASSWORD_COMMAND }, - request: { email }, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `${EXPIRE_PASSWORD_COMMAND} failed for "${maskEmail(email)}"`, - response.result_code || ResultCodes.USER_ACTION_FAILED - ) - } + const command: RestCommand = { + baseRequest: { command: EXPIRE_PASSWORD_COMMAND }, + request: { email }, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `${EXPIRE_PASSWORD_COMMAND} failed for "${maskEmail(email)}"`, + response.result_code || ResultCodes.USER_ACTION_FAILED, + ); + } } function chunkArray(values: T[], size: number): T[][] { - const chunks: T[][] = [] - for (let index = 0; index < values.length; index += size) { - chunks.push(values.slice(index, index + size)) - } - return chunks + const chunks: T[][] = []; + for (let index = 0; index < values.length; index += size) { + chunks.push(values.slice(index, index + size)); + } + return chunks; } function toEnterpriseUserId(value: unknown): number | null { - if (value == null) { - return null - } - const numericId = - typeof value === 'object' && 'toNumber' in value - ? (value as { toNumber(): number }).toNumber() - : Number(value) - return isNumber(numericId) && numericId > 0 ? numericId : null + if (value == null) { + return null; + } + const numericId = + typeof value === "object" && "toNumber" in value + ? (value as { toNumber(): number }).toNumber() + : Number(value); + return isNumber(numericId) && numericId > 0 ? numericId : null; } function maskEmail(email: string): string { - return email.includes('@') ? `***@${email.split('@')[1]}` : '***' + return email.includes("@") ? `***@${email.split("@")[1]}` : "***"; } -function resolveActionUserTargets(allUsers: EnterpriseUser[], identifiers: string[]): ActionUserTarget[] { - const byEmail = new Map() - const byId = new Map() - for (const user of allUsers) { - if (user.username) byEmail.set(user.username.toLowerCase(), user) - byId.set(user.enterprise_user_id, user) +function resolveActionUserTargets( + allUsers: EnterpriseUser[], + identifiers: string[], +): ActionUserTarget[] { + const byEmail = new Map(); + const byId = new Map(); + for (const user of allUsers) { + if (user.username) byEmail.set(user.username.toLowerCase(), user); + byId.set(user.enterprise_user_id, user); + } + + const result: ActionUserTarget[] = []; + const seen = new Set(); + + for (const identifier of identifiers) { + const trimmed = identifier.trim(); + const numericId = Number(trimmed); + let user: EnterpriseUser | undefined; + + if (Number.isInteger(numericId)) { + user = byId.get(numericId); } - - const result: ActionUserTarget[] = [] - const seen = new Set() - - for (const identifier of identifiers) { - const trimmed = identifier.trim() - const numericId = Number(trimmed) - let user: EnterpriseUser | undefined - - if (Number.isInteger(numericId)) { - user = byId.get(numericId) - } - if (!user) { - user = byEmail.get(trimmed.toLowerCase()) - } - if (!user) { - result.push({ kind: 'not_found', identifier: trimmed }) - continue - } - if (!seen.has(user.enterprise_user_id)) { - seen.add(user.enterprise_user_id) - result.push({ kind: 'user', user }) - } + if (!user) { + user = byEmail.get(trimmed.toLowerCase()); + } + if (!user) { + result.push({ kind: "not_found", identifier: trimmed }); + continue; } + if (!seen.has(user.enterprise_user_id)) { + seen.add(user.enterprise_user_id); + result.push({ kind: "user", user }); + } + } - return result + return result; } -function resolveCallerEnterpriseUserId(auth: Auth, allUsers: EnterpriseUser[]): number | null { - const username = auth.username.trim().toLowerCase() - if (!username) { - return null - } - const match = allUsers.find((user) => user.username?.trim().toLowerCase() === username) - return match?.enterprise_user_id ?? null +function resolveCallerEnterpriseUserId( + auth: Auth, + allUsers: EnterpriseUser[], +): number | null { + const username = auth.username.trim().toLowerCase(); + if (!username) { + return null; + } + const match = allUsers.find( + (user) => user.username?.trim().toLowerCase() === username, + ); + return match?.enterprise_user_id ?? null; } function finalizeResult(items: UserActionItemResult[]): UserActionResult { - let succeeded = 0, skipped = 0, failed = 0 - for (const item of items) { - if (item.status === UserActionStatus.Success) succeeded++ - else if (item.status === UserActionStatus.Skipped) skipped++ - else failed++ - } - return { success: failed === 0 && succeeded > 0, items, succeeded, skipped, failed } + let succeeded = 0, + skipped = 0, + failed = 0; + for (const item of items) { + if (item.status === UserActionStatus.Success) succeeded++; + else if (item.status === UserActionStatus.Skipped) skipped++; + else failed++; + } + return { + success: failed === 0 && succeeded > 0, + items, + succeeded, + skipped, + failed, + }; } -export function formatUserActionResult(result: UserActionResult): FormattedUserActionTable { - const rows = result.items.map((item, index) => [ - String(index + 1), - item.status, - item.username, - item.enterpriseUserId != null ? String(item.enterpriseUserId) : '', - item.message || item.skipReason || '', - ]) - return { - headers: [...USER_ACTION_TABLE_HEADERS], - rows, - summary: `Succeeded: ${result.succeeded} Skipped: ${result.skipped} Failed: ${result.failed}`, - } +export function formatUserActionResult( + result: UserActionResult, +): FormattedUserActionTable { + const rows = result.items.map((item, index) => [ + String(index + 1), + item.status, + item.username, + item.enterpriseUserId != null ? String(item.enterpriseUserId) : "", + item.message || item.skipReason || "", + ]); + return { + headers: [...USER_ACTION_TABLE_HEADERS], + rows, + summary: `Succeeded: ${result.succeeded} Skipped: ${result.skipped} Failed: ${result.failed}`, + }; } -export function renderUserActionAsciiTable(table: FormattedUserActionTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell.padEnd(widths[columnIndex]) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [ - formatRow(headers), - formatRow(widths.map((w) => '-'.repeat(w))), - ...rows.map(formatRow), - table.summary, - ] - return lines.join('\n') +export function renderUserActionAsciiTable( + table: FormattedUserActionTable, +): string { + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell.padEnd(widths[columnIndex]); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = [ + formatRow(headers), + formatRow(widths.map((w) => "-".repeat(w))), + ...rows.map(formatRow), + table.summary, + ]; + return lines.join("\n"); } diff --git a/KeeperSdk/src/users/addUser.ts b/KeeperSdk/src/users/addUser.ts index f0816063..e9e1f33d 100644 --- a/KeeperSdk/src/users/addUser.ts +++ b/KeeperSdk/src/users/addUser.ts @@ -1,287 +1,349 @@ import { - encryptObjectForStorage, - type Auth, - type KeeperResponse, - type RestCommand, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, isNumber, isValidEmail, KeeperSdkError, ResultCodes } from '../utils' + encryptObjectForStorage, + type Auth, + type KeeperResponse, + type RestCommand, +} from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseUser, -} from '../teams/enterpriseData' + extractErrorMessage, + isNumber, + isValidEmail, + KeeperSdkError, + ResultCodes, +} from "../utils"; import { - applyDecryptedNodeNames, - applyEnterpriseNameToRoot, - parentNeedsNameLookup, - resolveParentNode, -} from '../teams/teamUtils' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseUser, +} from "../teams/enterpriseData"; import { - EnterpriseUserStatus, - normalizeEmailInputs, - validateUserProfileFields, - AddUserStatus, - AddUserSkipReason, - type AddUserInput, - type AddUserItemResult, - type AddUserResult, - type FormatAddUserResultOptions, - type FormattedAddUserTable, -} from './userTypes' - -export { AddUserStatus, AddUserSkipReason } -export type { AddUserInput, AddUserItemResult, AddUserResult, FormatAddUserResultOptions, FormattedAddUserTable } - -const USER_ADD_COMMAND = 'enterprise_user_add' -const ALLOCATE_IDS_COMMAND = 'enterprise_allocate_ids' -const REINVITE_COMMAND = 'resend_enterprise_invite' + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveParentNode, +} from "../teams/teamUtils"; +import { + EnterpriseUserStatus, + normalizeEmailInputs, + validateUserProfileFields, + AddUserStatus, + AddUserSkipReason, + type AddUserInput, + type AddUserItemResult, + type AddUserResult, + type FormatAddUserResultOptions, + type FormattedAddUserTable, +} from "./userTypes"; + +export { AddUserStatus, AddUserSkipReason }; +export type { + AddUserInput, + AddUserItemResult, + AddUserResult, + FormatAddUserResultOptions, + FormattedAddUserTable, +}; + +const USER_ADD_COMMAND = "enterprise_user_add"; +const ALLOCATE_IDS_COMMAND = "enterprise_allocate_ids"; +const REINVITE_COMMAND = "resend_enterprise_invite"; const ADD_USER_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Users, -] - -const USER_TABLE_HEADERS = ['#', 'Status', 'Email', 'User ID', 'Node ID', 'Detail'] + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Users, +]; + +const USER_TABLE_HEADERS = [ + "#", + "Status", + "Email", + "User ID", + "Node ID", + "Detail", +]; type UserAddPayload = { - enterprise_user_id: number - enterprise_user_username: string - node_id: number - encrypted_data: string - full_name?: string - job_title?: string -} + enterprise_user_id: number; + enterprise_user_username: string; + node_id: number; + encrypted_data: string; + full_name?: string; + job_title?: string; +}; type AllocateIdsPayload = { - number_requested: number -} + number_requested: number; +}; type ReinvitePayload = { - enterprise_user_id: number -} + enterprise_user_id: number; +}; type AllocateIdsResponse = KeeperResponse & { - base_id: number -} - -export async function addUsers(auth: Auth, input: AddUserInput): Promise { - const rawEmails = [ - ...new Map( - normalizeEmailInputs(input.emails) - .map((e) => [e.toLowerCase(), e] as const) - ).values(), - ] - - if (rawEmails.length === 0) { - throw new KeeperSdkError('No emails provided.', ResultCodes.NO_USERS_TO_ADD) + base_id: number; +}; + +export async function addUsers( + auth: Auth, + input: AddUserInput, +): Promise { + const rawEmails = [ + ...new Map( + normalizeEmailInputs(input.emails).map( + (e) => [e.toLowerCase(), e] as const, + ), + ).values(), + ]; + + if (rawEmails.length === 0) { + throw new KeeperSdkError( + "No emails provided.", + ResultCodes.NO_USERS_TO_ADD, + ); + } + + const parentIdentifier = input.parent ?? null; + const needsNameLookup = parentNeedsNameLookup(parentIdentifier); + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(ADD_USER_INCLUDES); + const nodes = response.nodes || []; + const existingUsers = response.users || []; + + applyEnterpriseNameToRoot(nodes, response.enterprise_name); + + if (needsNameLookup) { + const displayNames = await enterpriseData.getDisplayNames(); + applyDecryptedNodeNames(nodes, displayNames.nodes); + await enterpriseData.decryptNodeNames(nodes); + } + + const parentNode = resolveParentNode(nodes, parentIdentifier); + const parentNodeId = parentNode.node_id; + const parentNodeName = + EnterpriseDataManager.getNodePath(nodes, parentNode.node_id, { + omitRoot: false, + }) || + (parentNode.displayName || "").trim() || + String(parentNode.node_id); + + const treeKey = await enterpriseData.getTreeKey(); + if (!treeKey) { + throw new KeeperSdkError( + "Enterprise tree key is unavailable.", + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE, + ); + } + + const existingByEmail = buildExistingByEmail(existingUsers); + const fullName = (input.fullName || "").trim() || undefined; + const jobTitle = (input.jobTitle || "").trim() || undefined; + validateUserProfileFields(fullName, jobTitle); + + const items: AddUserItemResult[] = []; + + for (const raw of rawEmails) { + const email = raw.toLowerCase(); + const item: AddUserItemResult = { + username: raw, + status: AddUserStatus.Failed, + }; + + if (!isValidEmail(email)) { + item.status = AddUserStatus.Skipped; + item.skipReason = AddUserSkipReason.InvalidEmail; + item.message = `"${raw}" is not a valid email address.`; + items.push(item); + continue; } - const parentIdentifier = input.parent ?? null - const needsNameLookup = parentNeedsNameLookup(parentIdentifier) - - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(ADD_USER_INCLUDES) - const nodes = response.nodes || [] - const existingUsers = response.users || [] - - applyEnterpriseNameToRoot(nodes, response.enterprise_name) - - if (needsNameLookup) { - const displayNames = await enterpriseData.getDisplayNames() - applyDecryptedNodeNames(nodes, displayNames.nodes) - await enterpriseData.decryptNodeNames(nodes) - } - - const parentNode = resolveParentNode(nodes, parentIdentifier) - const parentNodeId = parentNode.node_id - const parentNodeName = - EnterpriseDataManager.getNodePath(nodes, parentNode.node_id, { omitRoot: false }) || - (parentNode.displayName || '').trim() || - String(parentNode.node_id) - - const treeKey = await enterpriseData.getTreeKey() - if (!treeKey) { - throw new KeeperSdkError( - 'Enterprise tree key is unavailable.', - ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE - ) - } - - const existingByEmail = buildExistingByEmail(existingUsers) - const fullName = (input.fullName || '').trim() || undefined - const jobTitle = (input.jobTitle || '').trim() || undefined - validateUserProfileFields(fullName, jobTitle) - - const items: AddUserItemResult[] = [] + const existing = existingByEmail.get(email); + if (existing) { + item.enterpriseUserId = existing.enterprise_user_id; + item.nodeId = existing.node_id; - for (const raw of rawEmails) { - const email = raw.toLowerCase() - const item: AddUserItemResult = { - username: raw, - status: AddUserStatus.Failed, - } - - if (!isValidEmail(email)) { - item.status = AddUserStatus.Skipped - item.skipReason = AddUserSkipReason.InvalidEmail - item.message = `"${raw}" is not a valid email address.` - items.push(item) - continue - } - - const existing = existingByEmail.get(email) - if (existing) { - item.enterpriseUserId = existing.enterprise_user_id - item.nodeId = existing.node_id - - if (existing.status === EnterpriseUserStatus.Invited) { - try { - await sendReinvite(auth, existing.enterprise_user_id) - item.status = AddUserStatus.Reinvited - item.message = 'Invitation resent.' - } catch (err) { - item.message = extractErrorMessage(err) - } - } else { - item.status = AddUserStatus.Skipped - item.skipReason = AddUserSkipReason.AlreadyExists - item.message = `User "${raw}" has already accepted the invitation.` - } - items.push(item) - continue - } - - item.username = email + if (existing.status === EnterpriseUserStatus.Invited) { try { - const enterpriseUserId = await allocateEnterpriseId(auth) - const encryptedData = await encryptObjectForStorage({ displayname: fullName || '' }, treeKey) - await sendUserAdd(auth, { - enterprise_user_id: enterpriseUserId, - enterprise_user_username: email, - node_id: parentNodeId, - encrypted_data: encryptedData, - full_name: fullName, - job_title: jobTitle, - }) - item.enterpriseUserId = enterpriseUserId - item.nodeId = parentNodeId - item.status = AddUserStatus.Added + await sendReinvite(auth, existing.enterprise_user_id); + item.status = AddUserStatus.Reinvited; + item.message = "Invitation resent."; } catch (err) { - item.message = extractErrorMessage(err) + item.message = extractErrorMessage(err); } - items.push(item) + } else { + item.status = AddUserStatus.Skipped; + item.skipReason = AddUserSkipReason.AlreadyExists; + item.message = `User "${raw}" has already accepted the invitation.`; + } + items.push(item); + continue; + } + + item.username = email; + try { + const enterpriseUserId = await allocateEnterpriseId(auth); + const encryptedData = await encryptObjectForStorage( + { displayname: fullName || "" }, + treeKey, + ); + await sendUserAdd(auth, { + enterprise_user_id: enterpriseUserId, + enterprise_user_username: email, + node_id: parentNodeId, + encrypted_data: encryptedData, + full_name: fullName, + job_title: jobTitle, + }); + item.enterpriseUserId = enterpriseUserId; + item.nodeId = parentNodeId; + item.status = AddUserStatus.Added; + } catch (err) { + item.message = extractErrorMessage(err); } + items.push(item); + } - return finalizeResult(items, parentNodeId, parentNodeName) + return finalizeResult(items, parentNodeId, parentNodeName); } async function allocateEnterpriseId(auth: Auth): Promise { - const command: RestCommand = { - baseRequest: { command: ALLOCATE_IDS_COMMAND }, - request: { number_requested: 1 }, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - assertCommandSuccess(response, 'enterprise_allocate_ids failed') - if (!isNumber(response.base_id) || response.base_id === 0) { - throw new KeeperSdkError('Failed to allocate enterprise user ID.', ResultCodes.USER_ADD_FAILED) - } - return response.base_id + const command: RestCommand = { + baseRequest: { command: ALLOCATE_IDS_COMMAND }, + request: { number_requested: 1 }, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + assertCommandSuccess(response, "enterprise_allocate_ids failed"); + if (!isNumber(response.base_id) || response.base_id === 0) { + throw new KeeperSdkError( + "Failed to allocate enterprise user ID.", + ResultCodes.USER_ADD_FAILED, + ); + } + return response.base_id; } async function sendUserAdd(auth: Auth, payload: UserAddPayload): Promise { - const command: RestCommand = { - baseRequest: { command: USER_ADD_COMMAND }, - request: payload, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - assertCommandSuccess(response, `${USER_ADD_COMMAND} failed for "${payload.enterprise_user_username}"`) + const command: RestCommand = { + baseRequest: { command: USER_ADD_COMMAND }, + request: payload, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + assertCommandSuccess( + response, + `${USER_ADD_COMMAND} failed for "${payload.enterprise_user_username}"`, + ); } -async function sendReinvite(auth: Auth, enterpriseUserId: number): Promise { - const command: RestCommand = { - baseRequest: { command: REINVITE_COMMAND }, - request: { enterprise_user_id: enterpriseUserId }, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - assertCommandSuccess(response, `${REINVITE_COMMAND} failed for user_id=${enterpriseUserId}`) +async function sendReinvite( + auth: Auth, + enterpriseUserId: number, +): Promise { + const command: RestCommand = { + baseRequest: { command: REINVITE_COMMAND }, + request: { enterprise_user_id: enterpriseUserId }, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + assertCommandSuccess( + response, + `${REINVITE_COMMAND} failed for user_id=${enterpriseUserId}`, + ); } -function assertCommandSuccess(response: KeeperResponse, fallbackMessage: string): void { - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || response.result_code || fallbackMessage, - response.result_code || ResultCodes.USER_ADD_FAILED - ) - } +function assertCommandSuccess( + response: KeeperResponse, + fallbackMessage: string, +): void { + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || response.result_code || fallbackMessage, + response.result_code || ResultCodes.USER_ADD_FAILED, + ); + } } -function buildExistingByEmail(users: EnterpriseUser[]): Map { - const map = new Map() - for (const u of users) { - if (u.username) map.set(u.username.toLowerCase(), u) - } - return map +function buildExistingByEmail( + users: EnterpriseUser[], +): Map { + const map = new Map(); + for (const u of users) { + if (u.username) map.set(u.username.toLowerCase(), u); + } + return map; } function finalizeResult( - items: AddUserItemResult[], - parentNodeId: number, - parentNodeName: string + items: AddUserItemResult[], + parentNodeId: number, + parentNodeName: string, ): AddUserResult { - let added = 0, reinvited = 0, skipped = 0, failed = 0 - for (const item of items) { - if (item.status === AddUserStatus.Added) added++ - else if (item.status === AddUserStatus.Reinvited) reinvited++ - else if (item.status === AddUserStatus.Skipped) skipped++ - else failed++ - } - return { success: failed === 0 && (added > 0 || reinvited > 0), parentNodeId, parentNodeName, items, added, reinvited, skipped, failed } + let added = 0, + reinvited = 0, + skipped = 0, + failed = 0; + for (const item of items) { + if (item.status === AddUserStatus.Added) added++; + else if (item.status === AddUserStatus.Reinvited) reinvited++; + else if (item.status === AddUserStatus.Skipped) skipped++; + else failed++; + } + return { + success: failed === 0 && (added > 0 || reinvited > 0), + parentNodeId, + parentNodeName, + items, + added, + reinvited, + skipped, + failed, + }; } export function formatAddUserResult( - result: AddUserResult, - options: FormatAddUserResultOptions = {} + result: AddUserResult, + options: FormatAddUserResultOptions = {}, ): FormattedAddUserTable { - const showSkipped = options.showSkipped !== false - const visible = result.items.filter((item) => showSkipped || item.status !== AddUserStatus.Skipped) - - const rows = visible.map((item, index) => [ - String(index + 1), - item.status, - item.username, - item.enterpriseUserId != null ? String(item.enterpriseUserId) : '', - item.nodeId != null ? String(item.nodeId) : '', - item.message || item.skipReason || '', - ]) - - return { - headers: [...USER_TABLE_HEADERS], - rows, - parentNodeName: result.parentNodeName, - summary: `Added: ${result.added} Reinvited: ${result.reinvited} Skipped: ${result.skipped} Failed: ${result.failed}`, - } + const showSkipped = options.showSkipped !== false; + const visible = result.items.filter( + (item) => showSkipped || item.status !== AddUserStatus.Skipped, + ); + + const rows = visible.map((item, index) => [ + String(index + 1), + item.status, + item.username, + item.enterpriseUserId != null ? String(item.enterpriseUserId) : "", + item.nodeId != null ? String(item.nodeId) : "", + item.message || item.skipReason || "", + ]); + + return { + headers: [...USER_TABLE_HEADERS], + rows, + parentNodeName: result.parentNodeName, + summary: `Added: ${result.added} Reinvited: ${result.reinvited} Skipped: ${result.skipped} Failed: ${result.failed}`, + }; } export function renderAddUserAsciiTable(table: FormattedAddUserTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell.padEnd(widths[columnIndex]) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [ - `Parent: ${table.parentNodeName}`, - formatRow(headers), - formatRow(widths.map((w) => '-'.repeat(w))), - ...rows.map(formatRow), - table.summary, - ] - return lines.join('\n') + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell.padEnd(widths[columnIndex]); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = [ + `Parent: ${table.parentNodeName}`, + formatRow(headers), + formatRow(widths.map((w) => "-".repeat(w))), + ...rows.map(formatRow), + table.summary, + ]; + return lines.join("\n"); } diff --git a/KeeperSdk/src/users/aliasUser.ts b/KeeperSdk/src/users/aliasUser.ts index 90af0aa0..839c3b74 100644 --- a/KeeperSdk/src/users/aliasUser.ts +++ b/KeeperSdk/src/users/aliasUser.ts @@ -1,122 +1,177 @@ import { - enterpriseUserAddAliasMessage, - enterpriseUserDeleteAliasMessage, - enterpriseUserSetPrimaryAliasMessage, - type Auth, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, logger, ResultCodes } from '../utils' + enterpriseUserAddAliasMessage, + enterpriseUserDeleteAliasMessage, + enterpriseUserSetPrimaryAliasMessage, + type Auth, +} from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseUser, - type EnterpriseUserAliasLink, -} from '../teams/enterpriseData' + extractErrorMessage, + KeeperSdkError, + logger, + ResultCodes, +} from "../utils"; import { - resolveExistingUsers, - AliasOperation, - type AliasUserInput, - type AliasUserResult, -} from './userTypes' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseUser, + type EnterpriseUserAliasLink, +} from "../teams/enterpriseData"; +import { + resolveExistingUsers, + AliasOperation, + type AliasUserInput, + type AliasUserResult, +} from "./userTypes"; -export { AliasOperation } -export type { AliasUserInput, AliasUserResult } +export { AliasOperation }; +export type { AliasUserInput, AliasUserResult }; const ALIAS_USER_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Users, - EnterpriseDataInclude.UserAliases, -] - -export async function aliasUser(auth: Auth, input: AliasUserInput): Promise { - const alias = input.alias.trim().toLowerCase() + EnterpriseDataInclude.Users, + EnterpriseDataInclude.UserAliases, +]; - if (!alias) { - throw new KeeperSdkError('Alias is required.', ResultCodes.USER_ALIAS_FAILED) - } - - const identifier = input.email.trim() - if (!identifier) { - throw new KeeperSdkError('User email or ID is required.', ResultCodes.USER_NOT_FOUND) - } - - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(ALIAS_USER_INCLUDES) - - const [user] = resolveExistingUsers(response.users || [], [identifier]) - const existingAliases = (response.user_aliases || []).filter( - (a) => a.enterprise_user_id === user.enterprise_user_id - ) - - if (input.operation === AliasOperation.Add) { - return addAlias(auth, user, existingAliases, alias) - } - return removeAlias(auth, user, existingAliases, alias) +export async function aliasUser( + auth: Auth, + input: AliasUserInput, +): Promise { + const alias = input.alias.trim().toLowerCase(); + + if (!alias) { + throw new KeeperSdkError( + "Alias is required.", + ResultCodes.USER_ALIAS_FAILED, + ); + } + + const identifier = input.email.trim(); + if (!identifier) { + throw new KeeperSdkError( + "User email or ID is required.", + ResultCodes.USER_NOT_FOUND, + ); + } + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(ALIAS_USER_INCLUDES); + + const [user] = resolveExistingUsers(response.users || [], [identifier]); + const existingAliases = (response.user_aliases || []).filter( + (a) => a.enterprise_user_id === user.enterprise_user_id, + ); + + if (input.operation === AliasOperation.Add) { + return addAlias(auth, user, existingAliases, alias); + } + return removeAlias(auth, user, existingAliases, alias); } async function addAlias( - auth: Auth, - user: EnterpriseUser, - existingAliases: EnterpriseUserAliasLink[], - alias: string + auth: Auth, + user: EnterpriseUser, + existingAliases: EnterpriseUserAliasLink[], + alias: string, ): Promise { - const base = { username: user.username, enterpriseUserId: user.enterprise_user_id, alias, operation: AliasOperation.Add } - - if (user.username === alias) { - logger.info(`Alias "${alias}" is already the primary email for this user.`) - return { ...base, success: true, detail: 'Alias is already the primary email.' } + const base = { + username: user.username, + enterpriseUserId: user.enterprise_user_id, + alias, + operation: AliasOperation.Add, + }; + + if (user.username === alias) { + logger.info(`Alias "${alias}" is already the primary email for this user.`); + return { + ...base, + success: true, + detail: "Alias is already the primary email.", + }; + } + + const alreadyExists = existingAliases.some((a) => a.username === alias); + + try { + if (alreadyExists) { + await sendSetPrimaryAlias(auth, user.enterprise_user_id, alias); + return { ...base, success: true, detail: "Alias promoted to primary." }; } - const alreadyExists = existingAliases.some((a) => a.username === alias) - - try { - if (alreadyExists) { - await sendSetPrimaryAlias(auth, user.enterprise_user_id, alias) - return { ...base, success: true, detail: 'Alias promoted to primary.' } - } - - await sendAddAlias(auth, user.enterprise_user_id, alias) - return { ...base, success: true, detail: 'Alias added.' } - } catch (err) { - throw new KeeperSdkError(extractErrorMessage(err), ResultCodes.USER_ALIAS_FAILED) - } + await sendAddAlias(auth, user.enterprise_user_id, alias); + return { ...base, success: true, detail: "Alias added." }; + } catch (err) { + throw new KeeperSdkError( + extractErrorMessage(err), + ResultCodes.USER_ALIAS_FAILED, + ); + } } async function removeAlias( - auth: Auth, - user: EnterpriseUser, - existingAliases: EnterpriseUserAliasLink[], - alias: string + auth: Auth, + user: EnterpriseUser, + existingAliases: EnterpriseUserAliasLink[], + alias: string, ): Promise { - const base = { username: user.username, enterpriseUserId: user.enterprise_user_id, alias, operation: AliasOperation.Remove } - - const exists = alias === user.username || existingAliases.some((a) => a.username === alias) - - if (!exists) { - logger.info(`Alias "${alias}" does not exist for user.`) - return { ...base, success: false, detail: 'Alias does not exist.' } - } - try { - await sendDeleteAlias(auth, user.enterprise_user_id, alias) - return { ...base, success: true, detail: 'Alias removed.' } - } catch (err) { - throw new KeeperSdkError(extractErrorMessage(err), ResultCodes.USER_ALIAS_FAILED) - } + const base = { + username: user.username, + enterpriseUserId: user.enterprise_user_id, + alias, + operation: AliasOperation.Remove, + }; + + const exists = + alias === user.username || + existingAliases.some((a) => a.username === alias); + + if (!exists) { + logger.info(`Alias "${alias}" does not exist for user.`); + return { ...base, success: false, detail: "Alias does not exist." }; + } + try { + await sendDeleteAlias(auth, user.enterprise_user_id, alias); + return { ...base, success: true, detail: "Alias removed." }; + } catch (err) { + throw new KeeperSdkError( + extractErrorMessage(err), + ResultCodes.USER_ALIAS_FAILED, + ); + } } -async function sendAddAlias(auth: Auth, enterpriseUserId: number, alias: string): Promise { - const response = await auth.executeRest( - enterpriseUserAddAliasMessage({ enterpriseUserId, alias, primary: true }) - ) - for (const status of response.status || []) { - if (status.status && status.status !== 'success') { - throw new KeeperSdkError(`Add alias failed: ${status.status}`, ResultCodes.USER_ALIAS_FAILED) - } +async function sendAddAlias( + auth: Auth, + enterpriseUserId: number, + alias: string, +): Promise { + const response = await auth.executeRest( + enterpriseUserAddAliasMessage({ enterpriseUserId, alias, primary: true }), + ); + for (const status of response.status || []) { + if (status.status && status.status !== "success") { + throw new KeeperSdkError( + `Add alias failed: ${status.status}`, + ResultCodes.USER_ALIAS_FAILED, + ); } + } } -async function sendSetPrimaryAlias(auth: Auth, enterpriseUserId: number, alias: string): Promise { - await auth.executeRestAction(enterpriseUserSetPrimaryAliasMessage({ enterpriseUserId, alias })) +async function sendSetPrimaryAlias( + auth: Auth, + enterpriseUserId: number, + alias: string, +): Promise { + await auth.executeRestAction( + enterpriseUserSetPrimaryAliasMessage({ enterpriseUserId, alias }), + ); } -async function sendDeleteAlias(auth: Auth, enterpriseUserId: number, alias: string): Promise { - await auth.executeRestAction(enterpriseUserDeleteAliasMessage({ enterpriseUserId, alias })) +async function sendDeleteAlias( + auth: Auth, + enterpriseUserId: number, + alias: string, +): Promise { + await auth.executeRestAction( + enterpriseUserDeleteAliasMessage({ enterpriseUserId, alias }), + ); } diff --git a/KeeperSdk/src/users/deleteUser.ts b/KeeperSdk/src/users/deleteUser.ts index 327dd94e..f536dbdb 100644 --- a/KeeperSdk/src/users/deleteUser.ts +++ b/KeeperSdk/src/users/deleteUser.ts @@ -1,127 +1,148 @@ import { - type Auth, - type KeeperResponse, - type RestCommand, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' + type Auth, + type KeeperResponse, + type RestCommand, +} from "@keeper-security/keeperapi"; +import { extractErrorMessage, KeeperSdkError, ResultCodes } from "../utils"; import { - EnterpriseDataInclude, - EnterpriseDataManager, -} from '../teams/enterpriseData' + EnterpriseDataInclude, + EnterpriseDataManager, +} from "../teams/enterpriseData"; import { - normalizeEmailInputs, - resolveExistingUsers, - DeleteUserStatus, - type DeleteUserInput, - type DeleteUserItemResult, - type DeleteUserResult, - type FormattedDeleteUserTable, -} from './userTypes' - -export { DeleteUserStatus } -export type { DeleteUserInput, DeleteUserItemResult, DeleteUserResult, FormattedDeleteUserTable } - -const USER_DELETE_COMMAND = 'enterprise_user_delete' - -const DELETE_USER_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Users] + normalizeEmailInputs, + resolveExistingUsers, + DeleteUserStatus, + type DeleteUserInput, + type DeleteUserItemResult, + type DeleteUserResult, + type FormattedDeleteUserTable, +} from "./userTypes"; + +export { DeleteUserStatus }; +export type { + DeleteUserInput, + DeleteUserItemResult, + DeleteUserResult, + FormattedDeleteUserTable, +}; + +const USER_DELETE_COMMAND = "enterprise_user_delete"; + +const DELETE_USER_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Users, +]; type UserDeletePayload = { - enterprise_user_id: number -} - -export async function deleteUsers(auth: Auth, input: DeleteUserInput): Promise { - const rawEmails = normalizeEmailInputs(input.emails) - - if (rawEmails.length === 0) { - throw new KeeperSdkError('No users provided for deletion.', ResultCodes.NO_USERS_TO_DELETE) + enterprise_user_id: number; +}; + +export async function deleteUsers( + auth: Auth, + input: DeleteUserInput, +): Promise { + const rawEmails = normalizeEmailInputs(input.emails); + + if (rawEmails.length === 0) { + throw new KeeperSdkError( + "No users provided for deletion.", + ResultCodes.NO_USERS_TO_DELETE, + ); + } + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(DELETE_USER_INCLUDES); + const allUsers = response.users || []; + const resolvedUsers = resolveExistingUsers(allUsers, rawEmails); + + const items: DeleteUserItemResult[] = []; + + for (const user of resolvedUsers) { + const item: DeleteUserItemResult = { + username: user.username, + enterpriseUserId: user.enterprise_user_id, + status: DeleteUserStatus.Failed, + }; + + try { + await sendUserDelete(auth, user.enterprise_user_id); + item.status = DeleteUserStatus.Deleted; + } catch (err) { + item.message = extractErrorMessage(err); } + items.push(item); + } - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(DELETE_USER_INCLUDES) - const allUsers = response.users || [] - const resolvedUsers = resolveExistingUsers(allUsers, rawEmails) - - const items: DeleteUserItemResult[] = [] - - for (const user of resolvedUsers) { - const item: DeleteUserItemResult = { - username: user.username, - enterpriseUserId: user.enterprise_user_id, - status: DeleteUserStatus.Failed, - } - - try { - await sendUserDelete(auth, user.enterprise_user_id) - item.status = DeleteUserStatus.Deleted - } catch (err) { - item.message = extractErrorMessage(err) - } - items.push(item) - } - - return finalizeResult(items) + return finalizeResult(items); } -async function sendUserDelete(auth: Auth, enterpriseUserId: number): Promise { - const command: RestCommand = { - baseRequest: { command: USER_DELETE_COMMAND }, - request: { enterprise_user_id: enterpriseUserId }, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `${USER_DELETE_COMMAND} failed for user_id=${enterpriseUserId}`, - response.result_code || ResultCodes.USER_DELETE_FAILED - ) - } +async function sendUserDelete( + auth: Auth, + enterpriseUserId: number, +): Promise { + const command: RestCommand = { + baseRequest: { command: USER_DELETE_COMMAND }, + request: { enterprise_user_id: enterpriseUserId }, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `${USER_DELETE_COMMAND} failed for user_id=${enterpriseUserId}`, + response.result_code || ResultCodes.USER_DELETE_FAILED, + ); + } } function finalizeResult(items: DeleteUserItemResult[]): DeleteUserResult { - let deleted = 0, failed = 0 - for (const item of items) { - if (item.status === DeleteUserStatus.Deleted) deleted++ - else failed++ - } - return { success: failed === 0 && deleted > 0, items, deleted, failed } + let deleted = 0, + failed = 0; + for (const item of items) { + if (item.status === DeleteUserStatus.Deleted) deleted++; + else failed++; + } + return { success: failed === 0 && deleted > 0, items, deleted, failed }; } -const USER_DELETE_TABLE_HEADERS = ['#', 'Status', 'Email', 'User ID', 'Detail'] - -export function formatDeleteUserResult(result: DeleteUserResult): FormattedDeleteUserTable { - const rows = result.items.map((item, index) => [ - String(index + 1), - item.status, - item.username, - String(item.enterpriseUserId), - item.message || '', - ]) - - return { - headers: [...USER_DELETE_TABLE_HEADERS], - rows, - summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, - } +const USER_DELETE_TABLE_HEADERS = ["#", "Status", "Email", "User ID", "Detail"]; + +export function formatDeleteUserResult( + result: DeleteUserResult, +): FormattedDeleteUserTable { + const rows = result.items.map((item, index) => [ + String(index + 1), + item.status, + item.username, + String(item.enterpriseUserId), + item.message || "", + ]); + + return { + headers: [...USER_DELETE_TABLE_HEADERS], + rows, + summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, + }; } -export function renderDeleteUserAsciiTable(table: FormattedDeleteUserTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell.padEnd(widths[columnIndex]) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [ - formatRow(headers), - formatRow(widths.map((w) => '-'.repeat(w))), - ...rows.map(formatRow), - table.summary, - ] - return lines.join('\n') +export function renderDeleteUserAsciiTable( + table: FormattedDeleteUserTable, +): string { + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell.padEnd(widths[columnIndex]); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = [ + formatRow(headers), + formatRow(widths.map((w) => "-".repeat(w))), + ...rows.map(formatRow), + table.summary, + ]; + return lines.join("\n"); } diff --git a/KeeperSdk/src/users/listUsers.ts b/KeeperSdk/src/users/listUsers.ts index 6ec295cf..b8e81bda 100644 --- a/KeeperSdk/src/users/listUsers.ts +++ b/KeeperSdk/src/users/listUsers.ts @@ -1,459 +1,548 @@ -import type { Auth } from '@keeper-security/keeperapi' -import { isNumber, TOKEN_SEPARATOR_PATTERN } from '../utils' +import type { Auth } from "@keeper-security/keeperapi"; +import { isNumber, TOKEN_SEPARATOR_PATTERN } from "../utils"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseDisplayNames, - type EnterpriseNode, - type EnterpriseQueuedTeamRecord, - type EnterpriseQueuedTeamUserLink, - type EnterpriseRole, - type EnterpriseRoleTeamLink, - type EnterpriseRoleUserLink, - type EnterpriseTeamRecord, - type EnterpriseTeamUserLink, - type EnterpriseUser, - type EnterpriseUserAliasLink, -} from '../teams/enterpriseData' -import { applyDecryptedNodeNames } from '../teams/teamUtils' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseQueuedTeamRecord, + type EnterpriseQueuedTeamUserLink, + type EnterpriseRole, + type EnterpriseRoleTeamLink, + type EnterpriseRoleUserLink, + type EnterpriseTeamRecord, + type EnterpriseTeamUserLink, + type EnterpriseUser, + type EnterpriseUserAliasLink, +} from "../teams/enterpriseData"; +import { applyDecryptedNodeNames } from "../teams/teamUtils"; import { - formatUserStatus, - formatTransferStatus, - UserColumn, - type UserColumnInput, - type ListUsersOptions, - type ListUserRow, - type FormattedUsersTable, - type FormatUsersTableOptions, -} from './userTypes' - -export { UserColumn } -export type { UserColumnInput, ListUsersOptions, ListUserRow, FormattedUsersTable, FormatUsersTableOptions } - -export const SUPPORTED_USER_COLUMNS: readonly UserColumn[] = Object.values(UserColumn) + formatUserStatus, + formatTransferStatus, + UserColumn, + type UserColumnInput, + type ListUsersOptions, + type ListUserRow, + type FormattedUsersTable, + type FormatUsersTableOptions, +} from "./userTypes"; + +export { UserColumn }; +export type { + UserColumnInput, + ListUsersOptions, + ListUserRow, + FormattedUsersTable, + FormatUsersTableOptions, +}; + +export const SUPPORTED_USER_COLUMNS: readonly UserColumn[] = + Object.values(UserColumn); export const DEFAULT_USER_COLUMNS: readonly UserColumn[] = [ - UserColumn.Name, - UserColumn.Status, - UserColumn.TransferStatus, - UserColumn.Node, -] + UserColumn.Name, + UserColumn.Status, + UserColumn.TransferStatus, + UserColumn.Node, +]; -const NODE_PATH_SEPARATOR = '\\' -const MIN_ASCII_COL_WIDTH = 2 -const ALL_COLUMNS_WILDCARD = '*' +const NODE_PATH_SEPARATOR = "\\"; +const MIN_ASCII_COL_WIDTH = 2; +const ALL_COLUMNS_WILDCARD = "*"; const HEADER_BY_COLUMN: Record = { - [UserColumn.Name]: 'Name', - [UserColumn.Status]: 'Status', - [UserColumn.TransferStatus]: 'Transfer Status', - [UserColumn.Node]: 'Node', - [UserColumn.TeamCount]: 'Team Count', - [UserColumn.Teams]: 'Teams', - [UserColumn.QueuedTeamCount]: 'Queued Team Count', - [UserColumn.QueuedTeams]: 'Queued Teams', - [UserColumn.RoleCount]: 'Role Count', - [UserColumn.Roles]: 'Roles', - [UserColumn.Alias]: 'Alias', - [UserColumn.TwoFaEnabled]: '2FA Enabled', -} + [UserColumn.Name]: "Name", + [UserColumn.Status]: "Status", + [UserColumn.TransferStatus]: "Transfer Status", + [UserColumn.Node]: "Node", + [UserColumn.TeamCount]: "Team Count", + [UserColumn.Teams]: "Teams", + [UserColumn.QueuedTeamCount]: "Queued Team Count", + [UserColumn.QueuedTeams]: "Queued Teams", + [UserColumn.RoleCount]: "Role Count", + [UserColumn.Roles]: "Roles", + [UserColumn.Alias]: "Alias", + [UserColumn.TwoFaEnabled]: "2FA Enabled", +}; type DecorateContext = { - nodePaths: Map - userTeams: Map> - userQueuedTeams: Map> - userRoles: Map> - teamNameByUid: Map - queuedTeamNameByUid: Map - roleNameById: Map - userAliases: Map -} - -export async function listUsers(auth: Auth, options: ListUsersOptions = {}): Promise { - const columns = resolveColumns(options.columns) - const includes = includesForColumns(columns) - const wantsDisplayNames = columns.includes(UserColumn.Node) - - const enterpriseData = new EnterpriseDataManager(auth) - const emptyDisplayNames: EnterpriseDisplayNames = { nodes: new Map(), roles: new Map() } - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(includes), - wantsDisplayNames ? enterpriseData.getDisplayNames() : Promise.resolve(emptyDisplayNames), - ]) - - const nodes = response.nodes || [] - applyDecryptedNodeNames(nodes, displayNames.nodes) - - const context: DecorateContext = { - nodePaths: buildNodePathLookup(nodes), - userTeams: buildUserTeamMap(response.team_users || []), - userQueuedTeams: buildUserQueuedTeamMap(response.queued_team_users || []), - userRoles: buildUserRolesMap( - response.role_users || [], - response.role_teams || [], - response.team_users || [] - ), - teamNameByUid: buildTeamNameMap(response.teams || []), - queuedTeamNameByUid: buildTeamNameMap(response.queued_teams || []), - roleNameById: buildRoleNameMap(response.roles || [], displayNames.roles), - userAliases: buildUserAliasMap(response.user_aliases || []), - } - - const pattern = options.pattern?.trim() || null - const rows: ListUserRow[] = [] - for (const user of response.users || []) { - const row: ListUserRow = { - enterprise_user_id: user.enterprise_user_id, - username: user.username, - } - decorateRow(row, user, columns, context) - if (pattern && !rowMatchesPattern(row, pattern)) continue - rows.push(row) - } - - rows.sort((a, b) => a.username.localeCompare(b.username, undefined, { sensitivity: 'base' })) - return rows + nodePaths: Map; + userTeams: Map>; + userQueuedTeams: Map>; + userRoles: Map>; + teamNameByUid: Map; + queuedTeamNameByUid: Map; + roleNameById: Map; + userAliases: Map; +}; + +export async function listUsers( + auth: Auth, + options: ListUsersOptions = {}, +): Promise { + const columns = resolveColumns(options.columns); + const includes = includesForColumns(columns); + const wantsDisplayNames = columns.includes(UserColumn.Node); + + const enterpriseData = new EnterpriseDataManager(auth); + const emptyDisplayNames: EnterpriseDisplayNames = { + nodes: new Map(), + roles: new Map(), + }; + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(includes), + wantsDisplayNames + ? enterpriseData.getDisplayNames() + : Promise.resolve(emptyDisplayNames), + ]); + + const nodes = response.nodes || []; + applyDecryptedNodeNames(nodes, displayNames.nodes); + + const context: DecorateContext = { + nodePaths: buildNodePathLookup(nodes), + userTeams: buildUserTeamMap(response.team_users || []), + userQueuedTeams: buildUserQueuedTeamMap(response.queued_team_users || []), + userRoles: buildUserRolesMap( + response.role_users || [], + response.role_teams || [], + response.team_users || [], + ), + teamNameByUid: buildTeamNameMap(response.teams || []), + queuedTeamNameByUid: buildTeamNameMap(response.queued_teams || []), + roleNameById: buildRoleNameMap(response.roles || [], displayNames.roles), + userAliases: buildUserAliasMap(response.user_aliases || []), + }; + + const pattern = options.pattern?.trim() || null; + const rows: ListUserRow[] = []; + for (const user of response.users || []) { + const row: ListUserRow = { + enterprise_user_id: user.enterprise_user_id, + username: user.username, + }; + decorateRow(row, user, columns, context); + if (pattern && !rowMatchesPattern(row, pattern)) continue; + rows.push(row); + } + + rows.sort((a, b) => + a.username.localeCompare(b.username, undefined, { sensitivity: "base" }), + ); + return rows; } export function formatUsersTable( - rows: ListUserRow[], - options: FormatUsersTableOptions = {} + rows: ListUserRow[], + options: FormatUsersTableOptions = {}, ): FormattedUsersTable { - const columns = resolveColumns(options.columns) - const headers: string[] = ['#', 'User ID', 'Email', ...columns.map((col) => HEADER_BY_COLUMN[col])] - - const outRows: string[][] = rows.map((row, idx) => { - const cells: string[] = [String(idx + 1), String(row.enterprise_user_id), row.username] - for (const col of columns) cells.push(formatCell(row, col)) - return cells - }) - - return { headers, rows: outRows } + const columns = resolveColumns(options.columns); + const headers: string[] = [ + "#", + "User ID", + "Email", + ...columns.map((col) => HEADER_BY_COLUMN[col]), + ]; + + const outRows: string[][] = rows.map((row, idx) => { + const cells: string[] = [ + String(idx + 1), + String(row.enterprise_user_id), + row.username, + ]; + for (const col of columns) cells.push(formatCell(row, col)); + return cells; + }); + + return { headers, rows: outRows }; } export function renderUsersAsciiTable( - table: FormattedUsersTable, - options: { minColWidth?: number } = {} + table: FormattedUsersTable, + options: { minColWidth?: number } = {}, ): string { - const { minColWidth = MIN_ASCII_COL_WIDTH } = options - const { headers, rows } = table - const columnCount = headers.length - - const expandedRows: string[][][] = rows.map((row) => - row.map((cell) => (cell.includes('\n') ? cell.split('\n') : [cell])) - ) - - const columnWidths = new Array(columnCount).fill(0) + const { minColWidth = MIN_ASCII_COL_WIDTH } = options; + const { headers, rows } = table; + const columnCount = headers.length; + + const expandedRows: string[][][] = rows.map((row) => + row.map((cell) => (cell.includes("\n") ? cell.split("\n") : [cell])), + ); + + const columnWidths = new Array(columnCount).fill(0); + for (let ci = 0; ci < columnCount; ci += 1) { + columnWidths[ci] = Math.max(headers[ci].length, minColWidth); + } + for (const row of expandedRows) { for (let ci = 0; ci < columnCount; ci += 1) { - columnWidths[ci] = Math.max(headers[ci].length, minColWidth) - } - for (const row of expandedRows) { - for (let ci = 0; ci < columnCount; ci += 1) { - for (const line of row[ci]) { - columnWidths[ci] = Math.max(columnWidths[ci], line.length, minColWidth) - } - } + for (const line of row[ci]) { + columnWidths[ci] = Math.max(columnWidths[ci], line.length, minColWidth); + } } + } - const padCell = (cell: string, ci: number): string => cell.padEnd(columnWidths[ci]) - const formatPhysicalRow = (cells: string[]): string => - cells.map((cell, ci) => padCell(cell, ci)).join(' ') + const padCell = (cell: string, ci: number): string => + cell.padEnd(columnWidths[ci]); + const formatPhysicalRow = (cells: string[]): string => + cells.map((cell, ci) => padCell(cell, ci)).join(" "); - const ruleRow = formatPhysicalRow(columnWidths.map((w) => '-'.repeat(w))) - const lines: string[] = [formatPhysicalRow(headers), ruleRow] + const ruleRow = formatPhysicalRow(columnWidths.map((w) => "-".repeat(w))); + const lines: string[] = [formatPhysicalRow(headers), ruleRow]; - for (const row of expandedRows) { - const physicalLineCount = Math.max(...row.map((cell) => cell.length)) - for (let li = 0; li < physicalLineCount; li += 1) { - lines.push(formatPhysicalRow(row.map((cell) => cell[li] ?? ''))) - } + for (const row of expandedRows) { + const physicalLineCount = Math.max(...row.map((cell) => cell.length)); + for (let li = 0; li < physicalLineCount; li += 1) { + lines.push(formatPhysicalRow(row.map((cell) => cell[li] ?? ""))); } - return lines.join('\n') + } + return lines.join("\n"); } -function includesForColumns(columns: readonly UserColumn[]): EnterpriseDataInclude[] { - const set = new Set([EnterpriseDataInclude.Users]) - for (const col of columns) { - switch (col) { - case UserColumn.Node: - set.add(EnterpriseDataInclude.Nodes) - break - case UserColumn.TeamCount: - set.add(EnterpriseDataInclude.TeamUsers) - break - case UserColumn.Teams: - set.add(EnterpriseDataInclude.TeamUsers) - set.add(EnterpriseDataInclude.Teams) - break - case UserColumn.QueuedTeamCount: - set.add(EnterpriseDataInclude.QueuedTeamUsers) - break - case UserColumn.QueuedTeams: - set.add(EnterpriseDataInclude.QueuedTeamUsers) - set.add(EnterpriseDataInclude.QueuedTeams) - set.add(EnterpriseDataInclude.Teams) - break - case UserColumn.RoleCount: - case UserColumn.Roles: - set.add(EnterpriseDataInclude.RoleUsers) - set.add(EnterpriseDataInclude.RoleTeams) - set.add(EnterpriseDataInclude.TeamUsers) - if (col === UserColumn.Roles) set.add(EnterpriseDataInclude.Roles) - break - case UserColumn.Alias: - set.add(EnterpriseDataInclude.UserAliases) - break - } +function includesForColumns( + columns: readonly UserColumn[], +): EnterpriseDataInclude[] { + const set = new Set([EnterpriseDataInclude.Users]); + for (const col of columns) { + switch (col) { + case UserColumn.Node: + set.add(EnterpriseDataInclude.Nodes); + break; + case UserColumn.TeamCount: + set.add(EnterpriseDataInclude.TeamUsers); + break; + case UserColumn.Teams: + set.add(EnterpriseDataInclude.TeamUsers); + set.add(EnterpriseDataInclude.Teams); + break; + case UserColumn.QueuedTeamCount: + set.add(EnterpriseDataInclude.QueuedTeamUsers); + break; + case UserColumn.QueuedTeams: + set.add(EnterpriseDataInclude.QueuedTeamUsers); + set.add(EnterpriseDataInclude.QueuedTeams); + set.add(EnterpriseDataInclude.Teams); + break; + case UserColumn.RoleCount: + case UserColumn.Roles: + set.add(EnterpriseDataInclude.RoleUsers); + set.add(EnterpriseDataInclude.RoleTeams); + set.add(EnterpriseDataInclude.TeamUsers); + if (col === UserColumn.Roles) set.add(EnterpriseDataInclude.Roles); + break; + case UserColumn.Alias: + set.add(EnterpriseDataInclude.UserAliases); + break; } - return Array.from(set) + } + return Array.from(set); } -function resolveColumns(input: ListUsersOptions['columns']): UserColumn[] { - if (input == null) return [...DEFAULT_USER_COLUMNS] - if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_USER_COLUMNS] - - const requested = Array.isArray(input) ? input : input.split(',').map((p) => p.trim()) - const allowed = new Set(SUPPORTED_USER_COLUMNS) - const seen = new Set() - for (const col of requested) { - if (col && allowed.has(col)) seen.add(col as UserColumn) - } - if (seen.size === 0) return [...DEFAULT_USER_COLUMNS] - return SUPPORTED_USER_COLUMNS.filter((col) => seen.has(col)) +function resolveColumns(input: ListUsersOptions["columns"]): UserColumn[] { + if (input == null) return [...DEFAULT_USER_COLUMNS]; + if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_USER_COLUMNS]; + + const requested = Array.isArray(input) + ? input + : input.split(",").map((p) => p.trim()); + const allowed = new Set(SUPPORTED_USER_COLUMNS); + const seen = new Set(); + for (const col of requested) { + if (col && allowed.has(col)) seen.add(col as UserColumn); + } + if (seen.size === 0) return [...DEFAULT_USER_COLUMNS]; + return SUPPORTED_USER_COLUMNS.filter((col) => seen.has(col)); } function rowMatchesPattern(row: ListUserRow, pattern: string): boolean { - const lowered = pattern.toLowerCase() - const tokens: string[] = [String(row.enterprise_user_id)] - - const addText = (value: string | undefined): void => { - if (value) tokens.push(...value.toLowerCase().split(TOKEN_SEPARATOR_PATTERN).filter(Boolean)) - } - const addList = (values: string[] | undefined): void => { - values?.forEach((v) => addText(v)) - } - - addText(row.username) - addText(row.name) - addText(row.status) - addText(row.transfer_status) - addText(row.node) - if (row.team_count != null) tokens.push(String(row.team_count)) - if (row.queued_team_count != null) tokens.push(String(row.queued_team_count)) - if (row.role_count != null) tokens.push(String(row.role_count)) - if (row.tfa_enabled != null) tokens.push(String(row.tfa_enabled)) - addList(row.teams) - addList(row.queued_teams) - addList(row.roles) - addList(row.alias) - - return tokens.some((t) => t.includes(lowered)) + const lowered = pattern.toLowerCase(); + const tokens: string[] = [String(row.enterprise_user_id)]; + + const addText = (value: string | undefined): void => { + if (value) + tokens.push( + ...value.toLowerCase().split(TOKEN_SEPARATOR_PATTERN).filter(Boolean), + ); + }; + const addList = (values: string[] | undefined): void => { + values?.forEach((v) => addText(v)); + }; + + addText(row.username); + addText(row.name); + addText(row.status); + addText(row.transfer_status); + addText(row.node); + if (row.team_count != null) tokens.push(String(row.team_count)); + if (row.queued_team_count != null) tokens.push(String(row.queued_team_count)); + if (row.role_count != null) tokens.push(String(row.role_count)); + if (row.tfa_enabled != null) tokens.push(String(row.tfa_enabled)); + addList(row.teams); + addList(row.queued_teams); + addList(row.roles); + addList(row.alias); + + return tokens.some((t) => t.includes(lowered)); } function buildNodePathLookup(nodes: EnterpriseNode[]): Map { - return new Map( - nodes.map((node) => [ - node.node_id, - EnterpriseDataManager.getNodePath(nodes, node.node_id, { - omitRoot: true, - separator: NODE_PATH_SEPARATOR, - }), - ]) - ) + return new Map( + nodes.map((node) => [ + node.node_id, + EnterpriseDataManager.getNodePath(nodes, node.node_id, { + omitRoot: true, + separator: NODE_PATH_SEPARATOR, + }), + ]), + ); } -function buildUserTeamMap(links: EnterpriseTeamUserLink[]): Map> { - const map = new Map>() - for (const link of links) { - if (!link.team_uid) continue - const set = map.get(link.enterprise_user_id) - if (set) set.add(link.team_uid) - else map.set(link.enterprise_user_id, new Set([link.team_uid])) - } - return map +function buildUserTeamMap( + links: EnterpriseTeamUserLink[], +): Map> { + const map = new Map>(); + for (const link of links) { + if (!link.team_uid) continue; + const set = map.get(link.enterprise_user_id); + if (set) set.add(link.team_uid); + else map.set(link.enterprise_user_id, new Set([link.team_uid])); + } + return map; } -function buildUserQueuedTeamMap(links: EnterpriseQueuedTeamUserLink[]): Map> { - const map = new Map>() - for (const link of links) { - if (!link.team_uid) continue - const set = map.get(link.enterprise_user_id) - if (set) set.add(link.team_uid) - else map.set(link.enterprise_user_id, new Set([link.team_uid])) - } - return map +function buildUserQueuedTeamMap( + links: EnterpriseQueuedTeamUserLink[], +): Map> { + const map = new Map>(); + for (const link of links) { + if (!link.team_uid) continue; + const set = map.get(link.enterprise_user_id); + if (set) set.add(link.team_uid); + else map.set(link.enterprise_user_id, new Set([link.team_uid])); + } + return map; } function buildUserRolesMap( - roleUsers: EnterpriseRoleUserLink[], - roleTeams: EnterpriseRoleTeamLink[], - teamUsers: EnterpriseTeamUserLink[] + roleUsers: EnterpriseRoleUserLink[], + roleTeams: EnterpriseRoleTeamLink[], + teamUsers: EnterpriseTeamUserLink[], ): Map> { - const map = new Map>() - - for (const link of roleUsers) { - const set = map.get(link.enterprise_user_id) - if (set) set.add(link.role_id) - else map.set(link.enterprise_user_id, new Set([link.role_id])) + const map = new Map>(); + + for (const link of roleUsers) { + const set = map.get(link.enterprise_user_id); + if (set) set.add(link.role_id); + else map.set(link.enterprise_user_id, new Set([link.role_id])); + } + + const rolesForTeam = new Map>(); + for (const link of roleTeams) { + if (!link.team_uid) continue; + const set = rolesForTeam.get(link.team_uid); + if (set) set.add(link.role_id); + else rolesForTeam.set(link.team_uid, new Set([link.role_id])); + } + + for (const link of teamUsers) { + if (!link.team_uid) continue; + const teamRoles = rolesForTeam.get(link.team_uid); + if (!teamRoles) continue; + const userRoles = map.get(link.enterprise_user_id); + if (userRoles) { + for (const roleId of teamRoles) userRoles.add(roleId); + } else { + map.set(link.enterprise_user_id, new Set(teamRoles)); } + } - const rolesForTeam = new Map>() - for (const link of roleTeams) { - if (!link.team_uid) continue - const set = rolesForTeam.get(link.team_uid) - if (set) set.add(link.role_id) - else rolesForTeam.set(link.team_uid, new Set([link.role_id])) - } - - for (const link of teamUsers) { - if (!link.team_uid) continue - const teamRoles = rolesForTeam.get(link.team_uid) - if (!teamRoles) continue - const userRoles = map.get(link.enterprise_user_id) - if (userRoles) { - for (const roleId of teamRoles) userRoles.add(roleId) - } else { - map.set(link.enterprise_user_id, new Set(teamRoles)) - } - } - - return map + return map; } -function buildTeamNameMap(teams: Array): Map { - const map = new Map() - for (const team of teams) { - if (team.team_uid) map.set(team.team_uid, (team.name || team.team_uid).trim() || team.team_uid) - } - return map +function buildTeamNameMap( + teams: Array, +): Map { + const map = new Map(); + for (const team of teams) { + if (team.team_uid) + map.set( + team.team_uid, + (team.name || team.team_uid).trim() || team.team_uid, + ); + } + return map; } -function buildRoleNameMap(roles: EnterpriseRole[], decrypted: Map): Map { - const map = new Map() - for (const role of roles) { - if (!isNumber(role.role_id)) continue - const display = (role.displayName || decrypted.get(role.role_id) || '').trim() - map.set(role.role_id, display || String(role.role_id)) - } - return map +function buildRoleNameMap( + roles: EnterpriseRole[], + decrypted: Map, +): Map { + const map = new Map(); + for (const role of roles) { + if (!isNumber(role.role_id)) continue; + const display = ( + role.displayName || + decrypted.get(role.role_id) || + "" + ).trim(); + map.set(role.role_id, display || String(role.role_id)); + } + return map; } -function buildUserAliasMap(links: EnterpriseUserAliasLink[]): Map { - const map = new Map>() - for (const link of links) { - if (!link.username) continue - const set = map.get(link.enterprise_user_id) - if (set) set.add(link.username) - else map.set(link.enterprise_user_id, new Set([link.username])) - } - const result = new Map() - for (const [userId, aliases] of map) { - result.set(userId, [...aliases].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))) - } - return result +function buildUserAliasMap( + links: EnterpriseUserAliasLink[], +): Map { + const map = new Map>(); + for (const link of links) { + if (!link.username) continue; + const set = map.get(link.enterprise_user_id); + if (set) set.add(link.username); + else map.set(link.enterprise_user_id, new Set([link.username])); + } + const result = new Map(); + for (const [userId, aliases] of map) { + result.set( + userId, + [...aliases].sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ), + ); + } + return result; } -function resolveSortedNames(keys: Set, nameByKey: Map): string[] { - const names: string[] = [] - for (const key of keys) { - const name = nameByKey.get(key) - if (name) names.push(name) - } - return names.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) +function resolveSortedNames( + keys: Set, + nameByKey: Map, +): string[] { + const names: string[] = []; + for (const key of keys) { + const name = nameByKey.get(key); + if (name) names.push(name); + } + return names.sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ); } function resolveQueuedTeamNames( - teamUids: Set, - teamNameByUid: Map, - queuedTeamNameByUid: Map + teamUids: Set, + teamNameByUid: Map, + queuedTeamNameByUid: Map, ): string[] { - const names = new Set() - for (const uid of teamUids) { - const fromTeams = teamNameByUid.get(uid) - if (fromTeams) names.add(fromTeams) - const fromQueued = queuedTeamNameByUid.get(uid) - if (fromQueued) names.add(fromQueued) - } - return [...names].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + const names = new Set(); + for (const uid of teamUids) { + const fromTeams = teamNameByUid.get(uid); + if (fromTeams) names.add(fromTeams); + const fromQueued = queuedTeamNameByUid.get(uid); + if (fromQueued) names.add(fromQueued); + } + return [...names].sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ); } function decorateRow( - row: ListUserRow, - user: EnterpriseUser, - columns: readonly UserColumn[], - ctx: DecorateContext + row: ListUserRow, + user: EnterpriseUser, + columns: readonly UserColumn[], + ctx: DecorateContext, ): void { - for (const col of columns) { - switch (col) { - case UserColumn.Name: - row.name = (user.full_name || '').trim() - break - case UserColumn.Status: - row.status = formatUserStatus(user) - break - case UserColumn.TransferStatus: - row.transfer_status = formatTransferStatus(user) - break - case UserColumn.Node: - row.node = user.node_id != null ? (ctx.nodePaths.get(user.node_id) ?? '') : '' - break - case UserColumn.TeamCount: - row.team_count = ctx.userTeams.get(user.enterprise_user_id)?.size ?? 0 - break - case UserColumn.Teams: { - const teamUids = ctx.userTeams.get(user.enterprise_user_id) - row.teams = teamUids ? resolveSortedNames(teamUids, ctx.teamNameByUid) : [] - break - } - case UserColumn.QueuedTeamCount: - row.queued_team_count = ctx.userQueuedTeams.get(user.enterprise_user_id)?.size ?? 0 - break - case UserColumn.QueuedTeams: { - const queuedUids = ctx.userQueuedTeams.get(user.enterprise_user_id) - row.queued_teams = queuedUids - ? resolveQueuedTeamNames(queuedUids, ctx.teamNameByUid, ctx.queuedTeamNameByUid) - : [] - break - } - case UserColumn.RoleCount: - row.role_count = ctx.userRoles.get(user.enterprise_user_id)?.size ?? 0 - break - case UserColumn.Roles: { - const roleIds = ctx.userRoles.get(user.enterprise_user_id) - row.roles = roleIds ? resolveSortedNames(roleIds, ctx.roleNameById) : [] - break - } - case UserColumn.Alias: { - const aliases = ctx.userAliases.get(user.enterprise_user_id) - row.alias = aliases - ? aliases.filter((a) => a.toLowerCase() !== user.username.toLowerCase()) - : [] - break - } - case UserColumn.TwoFaEnabled: - row.tfa_enabled = user.tfa_enabled ?? false - break - } + for (const col of columns) { + switch (col) { + case UserColumn.Name: + row.name = (user.full_name || "").trim(); + break; + case UserColumn.Status: + row.status = formatUserStatus(user); + break; + case UserColumn.TransferStatus: + row.transfer_status = formatTransferStatus(user); + break; + case UserColumn.Node: + row.node = + user.node_id != null ? (ctx.nodePaths.get(user.node_id) ?? "") : ""; + break; + case UserColumn.TeamCount: + row.team_count = ctx.userTeams.get(user.enterprise_user_id)?.size ?? 0; + break; + case UserColumn.Teams: { + const teamUids = ctx.userTeams.get(user.enterprise_user_id); + row.teams = teamUids + ? resolveSortedNames(teamUids, ctx.teamNameByUid) + : []; + break; + } + case UserColumn.QueuedTeamCount: + row.queued_team_count = + ctx.userQueuedTeams.get(user.enterprise_user_id)?.size ?? 0; + break; + case UserColumn.QueuedTeams: { + const queuedUids = ctx.userQueuedTeams.get(user.enterprise_user_id); + row.queued_teams = queuedUids + ? resolveQueuedTeamNames( + queuedUids, + ctx.teamNameByUid, + ctx.queuedTeamNameByUid, + ) + : []; + break; + } + case UserColumn.RoleCount: + row.role_count = ctx.userRoles.get(user.enterprise_user_id)?.size ?? 0; + break; + case UserColumn.Roles: { + const roleIds = ctx.userRoles.get(user.enterprise_user_id); + row.roles = roleIds + ? resolveSortedNames(roleIds, ctx.roleNameById) + : []; + break; + } + case UserColumn.Alias: { + const aliases = ctx.userAliases.get(user.enterprise_user_id); + row.alias = aliases + ? aliases.filter( + (a) => a.toLowerCase() !== user.username.toLowerCase(), + ) + : []; + break; + } + case UserColumn.TwoFaEnabled: + row.tfa_enabled = user.tfa_enabled ?? false; + break; } + } } function formatListCell(values: string[] | undefined): string { - return values && values.length > 0 ? values.join('\n') : '' + return values && values.length > 0 ? values.join("\n") : ""; } function formatCell(row: ListUserRow, col: UserColumn): string { - switch (col) { - case UserColumn.Name: return row.name ?? '' - case UserColumn.Status: return row.status ?? '' - case UserColumn.TransferStatus: return row.transfer_status ?? '' - case UserColumn.Node: return row.node ?? '' - case UserColumn.TeamCount: return row.team_count == null ? '' : String(row.team_count) - case UserColumn.Teams: return formatListCell(row.teams) - case UserColumn.QueuedTeamCount: return row.queued_team_count == null ? '' : String(row.queued_team_count) - case UserColumn.QueuedTeams: return formatListCell(row.queued_teams) - case UserColumn.RoleCount: return row.role_count == null ? '' : String(row.role_count) - case UserColumn.Roles: return formatListCell(row.roles) - case UserColumn.Alias: return formatListCell(row.alias) - case UserColumn.TwoFaEnabled: return row.tfa_enabled == null ? '' : String(row.tfa_enabled) - } + switch (col) { + case UserColumn.Name: + return row.name ?? ""; + case UserColumn.Status: + return row.status ?? ""; + case UserColumn.TransferStatus: + return row.transfer_status ?? ""; + case UserColumn.Node: + return row.node ?? ""; + case UserColumn.TeamCount: + return row.team_count == null ? "" : String(row.team_count); + case UserColumn.Teams: + return formatListCell(row.teams); + case UserColumn.QueuedTeamCount: + return row.queued_team_count == null ? "" : String(row.queued_team_count); + case UserColumn.QueuedTeams: + return formatListCell(row.queued_teams); + case UserColumn.RoleCount: + return row.role_count == null ? "" : String(row.role_count); + case UserColumn.Roles: + return formatListCell(row.roles); + case UserColumn.Alias: + return formatListCell(row.alias); + case UserColumn.TwoFaEnabled: + return row.tfa_enabled == null ? "" : String(row.tfa_enabled); + } } diff --git a/KeeperSdk/src/users/teamUser.ts b/KeeperSdk/src/users/teamUser.ts index 93d7838f..4e8f6e23 100644 --- a/KeeperSdk/src/users/teamUser.ts +++ b/KeeperSdk/src/users/teamUser.ts @@ -1,699 +1,876 @@ import { - type Auth, - type Authentication, - Enterprise, - type KeeperResponse, - type RestCommand, - decryptKey, - getPublicKeysMessage, - normal64Bytes, - platform, - teamQueueUserCommand, - teamsEnterpriseUsersAdd, - teamsEnterpriseUsersRemove, - webSafe64FromBytes, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, isNumber, KeeperSdkError, logger, ResultCodes } from '../utils' + type Auth, + type Authentication, + Enterprise, + type KeeperResponse, + type RestCommand, + decryptKey, + getPublicKeysMessage, + normal64Bytes, + platform, + teamQueueUserCommand, + teamsEnterpriseUsersAdd, + teamsEnterpriseUsersRemove, + webSafe64FromBytes, +} from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseQueuedTeamRecord, - type EnterpriseTeamRecord, - type EnterpriseUser, - type EnterpriseTeamUserLink, -} from '../teams/enterpriseData' -import { resolveExistingTeams } from '../teams/teamUtils' + extractErrorMessage, + isNumber, + KeeperSdkError, + logger, + ResultCodes, +} from "../utils"; import { - normalizeEmailInputs, - resolveExistingUsers, - EnterpriseUserStatus, - TeamUserStatus, - TeamUserSkipReason, - type AddUsersToTeamsInput, - type RemoveUsersFromTeamsInput, - type TeamUserItemResult, - type TeamUserResult, - type FormattedTeamUserTable, -} from './userTypes' - -export { TeamUserStatus, TeamUserSkipReason } + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseQueuedTeamRecord, + type EnterpriseTeamRecord, + type EnterpriseUser, + type EnterpriseTeamUserLink, +} from "../teams/enterpriseData"; +import { resolveExistingTeams } from "../teams/teamUtils"; +import { + normalizeEmailInputs, + resolveExistingUsers, + EnterpriseUserStatus, + TeamUserStatus, + TeamUserSkipReason, + type AddUsersToTeamsInput, + type RemoveUsersFromTeamsInput, + type TeamUserItemResult, + type TeamUserResult, + type FormattedTeamUserTable, +} from "./userTypes"; + +export { TeamUserStatus, TeamUserSkipReason }; export type { - AddUsersToTeamsInput, - RemoveUsersFromTeamsInput, - TeamUserItemResult, - TeamUserResult, - FormattedTeamUserTable, -} - -const SUCCESS_RESULT = 'success' -const MAX_TEAM_USERS_PER_REQUEST = 1000 -const MAX_USERS_PER_OPERATION = MAX_TEAM_USERS_PER_REQUEST -const MAX_TEAMS_PER_OPERATION = 100 -const MAX_RESPONSE_MESSAGE_LENGTH = 500 -const UNEXPECTED_TEAM_RESPONSE_MESSAGE = 'Unexpected API response: no team results returned' -const MISSING_TEAM_RESPONSE_MESSAGE = 'Team add response missing for this team' -const UNEXPECTED_REMOVE_RESPONSE_MESSAGE = 'Unexpected API response: no remove results returned' -const MISSING_REMOVE_RESPONSE_MESSAGE = 'Team remove response missing for this user' + AddUsersToTeamsInput, + RemoveUsersFromTeamsInput, + TeamUserItemResult, + TeamUserResult, + FormattedTeamUserTable, +}; + +const SUCCESS_RESULT = "success"; +const MAX_TEAM_USERS_PER_REQUEST = 1000; +const MAX_USERS_PER_OPERATION = MAX_TEAM_USERS_PER_REQUEST; +const MAX_TEAMS_PER_OPERATION = 100; +const MAX_RESPONSE_MESSAGE_LENGTH = 500; +const UNEXPECTED_TEAM_RESPONSE_MESSAGE = + "Unexpected API response: no team results returned"; +const MISSING_TEAM_RESPONSE_MESSAGE = "Team add response missing for this team"; +const UNEXPECTED_REMOVE_RESPONSE_MESSAGE = + "Unexpected API response: no remove results returned"; +const MISSING_REMOVE_RESPONSE_MESSAGE = + "Team remove response missing for this user"; const TEAM_USER_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Users, - EnterpriseDataInclude.Teams, - EnterpriseDataInclude.QueuedTeams, - EnterpriseDataInclude.TeamUsers, - EnterpriseDataInclude.QueuedTeamUsers, -] - -const TEAM_USER_TABLE_HEADERS = ['#', 'Status', 'User Email', 'User ID', 'Team Name', 'Team UID', 'Detail'] - -type ResolvedTeam = Pick + EnterpriseDataInclude.Users, + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.QueuedTeams, + EnterpriseDataInclude.TeamUsers, + EnterpriseDataInclude.QueuedTeamUsers, +]; + +const TEAM_USER_TABLE_HEADERS = [ + "#", + "Status", + "User Email", + "User ID", + "Team Name", + "Team UID", + "Detail", +]; + +type ResolvedTeam = Pick< + EnterpriseTeamRecord, + "team_uid" | "name" | "encrypted_team_key" +>; type UserPublicKeys = { - rsaPublicKey: Uint8Array | null - eccPublicKey: Uint8Array | null - errorCode?: string - message?: string -} + rsaPublicKey: Uint8Array | null; + eccPublicKey: Uint8Array | null; + errorCode?: string; + message?: string; +}; type EncryptedTeamKey = { - key: Uint8Array - keyType: Enterprise.EncryptedKeyType -} + key: Uint8Array; + keyType: Enterprise.EncryptedKeyType; +}; type PreparedBatchUser = { - user: EnterpriseUser - encryptedKey: EncryptedTeamKey -} + user: EnterpriseUser; + encryptedKey: EncryptedTeamKey; +}; type PreparedBatchTeam = { - team: ResolvedTeam - prepared: PreparedBatchUser[] -} + team: ResolvedTeam; + prepared: PreparedBatchUser[]; +}; type TeamUserContext = { - enterpriseData: EnterpriseDataManager - teams: ResolvedTeam[] - users: EnterpriseUser[] - membership: Set -} + enterpriseData: EnterpriseDataManager; + teams: ResolvedTeam[]; + users: EnterpriseUser[]; + membership: Set; +}; -type InvitedQueueEntry = { user: EnterpriseUser; team: ResolvedTeam } +type InvitedQueueEntry = { user: EnterpriseUser; team: ResolvedTeam }; type AddBatchPreparation = { - items: TeamUserItemResult[] - batchTeams: PreparedBatchTeam[] - invitedQueue: InvitedQueueEntry[] -} + items: TeamUserItemResult[]; + batchTeams: PreparedBatchTeam[]; + invitedQueue: InvitedQueueEntry[]; +}; -type PreparedRemoveEntry = { user: EnterpriseUser; team: ResolvedTeam } +type PreparedRemoveEntry = { user: EnterpriseUser; team: ResolvedTeam }; type RemoveBatchPreparation = { - items: TeamUserItemResult[] - toRemove: PreparedRemoveEntry[] -} + items: TeamUserItemResult[]; + toRemove: PreparedRemoveEntry[]; +}; -type TeamUserItemBase = Omit +type TeamUserItemBase = Omit; -const membershipKey = (userId: number, teamUid: string): string => `${userId}:${teamUid}` +const membershipKey = (userId: number, teamUid: string): string => + `${userId}:${teamUid}`; -const buildItemBase = (user: EnterpriseUser, team: ResolvedTeam): TeamUserItemBase => ({ - username: user.username, - enterpriseUserId: user.enterprise_user_id, - teamUid: team.team_uid, - teamName: team.name, -}) +const buildItemBase = ( + user: EnterpriseUser, + team: ResolvedTeam, +): TeamUserItemBase => ({ + username: user.username, + enterpriseUserId: user.enterprise_user_id, + teamUid: team.team_uid, + teamName: team.name, +}); const toResolvedTeam = (team: EnterpriseTeamRecord): ResolvedTeam => ({ - team_uid: team.team_uid, - name: team.name, - encrypted_team_key: team.encrypted_team_key, -}) - -const toQueuedTeamRecord = (team: EnterpriseQueuedTeamRecord): EnterpriseTeamRecord => ({ - team_uid: team.team_uid, - name: team.name, - node_id: team.node_id, -}) - -const toPublicKeyBytes = (value: Uint8Array | null | undefined): Uint8Array | null => - value && value.length > 0 ? value : null + team_uid: team.team_uid, + name: team.name, + encrypted_team_key: team.encrypted_team_key, +}); + +const toQueuedTeamRecord = ( + team: EnterpriseQueuedTeamRecord, +): EnterpriseTeamRecord => ({ + team_uid: team.team_uid, + name: team.name, + node_id: team.node_id, +}); + +const toPublicKeyBytes = ( + value: Uint8Array | null | undefined, +): Uint8Array | null => (value && value.length > 0 ? value : null); const normalizeTeamIdentifiers = (teams: string[] | undefined): string[] => - (teams || []).map((t) => t.trim()).filter((t) => t.length > 0) + (teams || []).map((t) => t.trim()).filter((t) => t.length > 0); function buildMembershipSet( - teamUsers: EnterpriseTeamUserLink[] | undefined, - queuedTeamUsers: EnterpriseTeamUserLink[] | undefined + teamUsers: EnterpriseTeamUserLink[] | undefined, + queuedTeamUsers: EnterpriseTeamUserLink[] | undefined, ): Set { - const membership = new Set() - for (const link of [...(teamUsers || []), ...(queuedTeamUsers || [])]) { - membership.add(membershipKey(link.enterprise_user_id, link.team_uid)) - } - return membership -} - -async function fetchUserPublicKeys(auth: Auth, emails: string[]): Promise> { - const result = new Map() - if (emails.length === 0) return result - - let response: Authentication.IGetPublicKeysResponse - try { - response = await auth.executeRest(getPublicKeysMessage({ usernames: emails })) - } catch (err) { - throw new KeeperSdkError( - `Failed to fetch public keys: ${extractErrorMessage(err)}`, - ResultCodes.TEAM_USER_ADD_FAILED - ) - } - - for (const entry of response.keyResponses || []) { - const username = (entry.username || '').toLowerCase() - if (!username) continue - result.set(username, { - rsaPublicKey: toPublicKeyBytes(entry.publicKey ?? undefined), - eccPublicKey: toPublicKeyBytes(entry.publicEccKey ?? undefined), - errorCode: entry.errorCode || undefined, - message: entry.message || undefined, - }) - } - return result + const membership = new Set(); + for (const link of [...(teamUsers || []), ...(queuedTeamUsers || [])]) { + membership.add(membershipKey(link.enterprise_user_id, link.team_uid)); + } + return membership; +} + +async function fetchUserPublicKeys( + auth: Auth, + emails: string[], +): Promise> { + const result = new Map(); + if (emails.length === 0) return result; + + let response: Authentication.IGetPublicKeysResponse; + try { + response = await auth.executeRest( + getPublicKeysMessage({ usernames: emails }), + ); + } catch (err) { + throw new KeeperSdkError( + `Failed to fetch public keys: ${extractErrorMessage(err)}`, + ResultCodes.TEAM_USER_ADD_FAILED, + ); + } + + for (const entry of response.keyResponses || []) { + const username = (entry.username || "").toLowerCase(); + if (!username) continue; + result.set(username, { + rsaPublicKey: toPublicKeyBytes(entry.publicKey ?? undefined), + eccPublicKey: toPublicKeyBytes(entry.publicEccKey ?? undefined), + errorCode: entry.errorCode || undefined, + message: entry.message || undefined, + }); + } + return result; } async function encryptTeamKeyForUser( - teamKey: Uint8Array, - publicKeys: UserPublicKeys + teamKey: Uint8Array, + publicKeys: UserPublicKeys, ): Promise { - if (publicKeys.rsaPublicKey) { - return { - key: platform.publicEncrypt(teamKey, platform.bytesToBase64(publicKeys.rsaPublicKey)), - keyType: Enterprise.EncryptedKeyType.KT_ENCRYPTED_BY_PUBLIC_KEY, - } - } + if (publicKeys.rsaPublicKey) { return { - key: await platform.publicEncryptEC(teamKey, publicKeys.eccPublicKey as Uint8Array), - keyType: Enterprise.EncryptedKeyType.KT_ENCRYPTED_BY_PUBLIC_KEY_ECC, - } + key: platform.publicEncrypt( + teamKey, + platform.bytesToBase64(publicKeys.rsaPublicKey), + ), + keyType: Enterprise.EncryptedKeyType.KT_ENCRYPTED_BY_PUBLIC_KEY, + }; + } + return { + key: await platform.publicEncryptEC( + teamKey, + publicKeys.eccPublicKey as Uint8Array, + ), + keyType: Enterprise.EncryptedKeyType.KT_ENCRYPTED_BY_PUBLIC_KEY_ECC, + }; } async function getDecryptedTeamKey( - team: ResolvedTeam, - treeKey: Uint8Array, - cache: Map + team: ResolvedTeam, + treeKey: Uint8Array, + cache: Map, ): Promise { - if (cache.has(team.team_uid)) return cache.get(team.team_uid) ?? null - if (!team.encrypted_team_key) { - cache.set(team.team_uid, null) - return null - } - try { - const key = await decryptKey(team.encrypted_team_key, treeKey) - cache.set(team.team_uid, key) - return key - } catch (err) { - logger.warn(`Could not decrypt key for team "${team.name}": ${extractErrorMessage(err)}`) - cache.set(team.team_uid, null) - return null - } + if (cache.has(team.team_uid)) return cache.get(team.team_uid) ?? null; + if (!team.encrypted_team_key) { + cache.set(team.team_uid, null); + return null; + } + try { + const key = await decryptKey(team.encrypted_team_key, treeKey); + cache.set(team.team_uid, key); + return key; + } catch (err) { + logger.warn( + `Could not decrypt key for team "${team.name}": ${extractErrorMessage(err)}`, + ); + cache.set(team.team_uid, null); + return null; + } } async function loadTeamUserContext( - auth: Auth, - rawUsers: string[], - rawTeams: string[] + auth: Auth, + rawUsers: string[], + rawTeams: string[], ): Promise { - const emails = normalizeEmailInputs(rawUsers) - if (emails.length === 0) { - throw new KeeperSdkError('No users provided.', ResultCodes.NO_USERS_TO_UPDATE) - } - const teamIdentifiers = normalizeTeamIdentifiers(rawTeams) - if (teamIdentifiers.length === 0) { - throw new KeeperSdkError('No teams provided.', ResultCodes.NO_TEAMS_FOR_USER_OP) - } - validateOperationLimits(emails.length, teamIdentifiers.length) - - const enterpriseData = new EnterpriseDataManager(auth) - const response = await enterpriseData.getData(TEAM_USER_INCLUDES) - const queuedTeams = (response.queued_teams || []).map(toQueuedTeamRecord) - - return { - enterpriseData, - teams: resolveExistingTeams(response.teams || [], teamIdentifiers, queuedTeams).map(toResolvedTeam), - users: resolveExistingUsers(response.users || [], emails), - membership: buildMembershipSet(response.team_users, response.queued_team_users), - } -} - -function determineUserType(hideSharedFolders: boolean | undefined): Enterprise.TeamUserType | undefined { - if (hideSharedFolders === true) return Enterprise.TeamUserType.ADMIN_ONLY - if (hideSharedFolders === false) return Enterprise.TeamUserType.USER - return undefined -} - -function pickPublicKeyError(publicKeys: UserPublicKeys | undefined, username: string): string { - return publicKeys?.message || publicKeys?.errorCode || `No public key for "${username}"` -} - -function hasUsablePublicKey(publicKeys: UserPublicKeys | undefined): publicKeys is UserPublicKeys { - return !!publicKeys && !publicKeys.errorCode && !!(publicKeys.eccPublicKey || publicKeys.rsaPublicKey) + const emails = normalizeEmailInputs(rawUsers); + if (emails.length === 0) { + throw new KeeperSdkError( + "No users provided.", + ResultCodes.NO_USERS_TO_UPDATE, + ); + } + const teamIdentifiers = normalizeTeamIdentifiers(rawTeams); + if (teamIdentifiers.length === 0) { + throw new KeeperSdkError( + "No teams provided.", + ResultCodes.NO_TEAMS_FOR_USER_OP, + ); + } + validateOperationLimits(emails.length, teamIdentifiers.length); + + const enterpriseData = new EnterpriseDataManager(auth); + const response = await enterpriseData.getData(TEAM_USER_INCLUDES); + const queuedTeams = (response.queued_teams || []).map(toQueuedTeamRecord); + + return { + enterpriseData, + teams: resolveExistingTeams( + response.teams || [], + teamIdentifiers, + queuedTeams, + ).map(toResolvedTeam), + users: resolveExistingUsers(response.users || [], emails), + membership: buildMembershipSet( + response.team_users, + response.queued_team_users, + ), + }; +} + +function determineUserType( + hideSharedFolders: boolean | undefined, +): Enterprise.TeamUserType | undefined { + if (hideSharedFolders === true) return Enterprise.TeamUserType.ADMIN_ONLY; + if (hideSharedFolders === false) return Enterprise.TeamUserType.USER; + return undefined; +} + +function pickPublicKeyError( + publicKeys: UserPublicKeys | undefined, + username: string, +): string { + return ( + publicKeys?.message || + publicKeys?.errorCode || + `No public key for "${username}"` + ); +} + +function hasUsablePublicKey( + publicKeys: UserPublicKeys | undefined, +): publicKeys is UserPublicKeys { + return ( + !!publicKeys && + !publicKeys.errorCode && + !!(publicKeys.eccPublicKey || publicKeys.rsaPublicKey) + ); } function sanitizeResponseMessage(value: string): string { - return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').slice(0, MAX_RESPONSE_MESSAGE_LENGTH) + return value + .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "") + .slice(0, MAX_RESPONSE_MESSAGE_LENGTH); } function pickResponseError( - message?: string | null, - resultCode?: string | null, - additionalInfo?: string | null, - fallback?: string + message?: string | null, + resultCode?: string | null, + additionalInfo?: string | null, + fallback?: string, ): string | undefined { - for (const candidate of [message, resultCode, additionalInfo, fallback]) { - if (typeof candidate === 'string' && candidate.trim().length > 0) { - return sanitizeResponseMessage(candidate.trim()) - } + for (const candidate of [message, resultCode, additionalInfo, fallback]) { + if (typeof candidate === "string" && candidate.trim().length > 0) { + return sanitizeResponseMessage(candidate.trim()); } - return undefined + } + return undefined; } function toEnterpriseUserId(value: unknown): number | null { - const id = typeof value === 'number' ? value : Number(value) - return isNumber(id) && id > 0 ? Math.trunc(id) : null + const id = typeof value === "number" ? value : Number(value); + return isNumber(id) && id > 0 ? Math.trunc(id) : null; } function validateOperationLimits(userCount: number, teamCount: number): void { - if (userCount > MAX_USERS_PER_OPERATION) { - throw new KeeperSdkError( - `Cannot process more than ${MAX_USERS_PER_OPERATION} users at once.`, - ResultCodes.NO_USERS_TO_UPDATE - ) - } - if (teamCount > MAX_TEAMS_PER_OPERATION) { - throw new KeeperSdkError( - `Cannot process more than ${MAX_TEAMS_PER_OPERATION} teams at once.`, - ResultCodes.NO_TEAMS_FOR_USER_OP - ) - } -} - -function validateBatchEntryCount(count: number, operation: 'add' | 'remove'): void { - if (count > MAX_TEAM_USERS_PER_REQUEST) { - const code = operation === 'add' ? ResultCodes.TEAM_USER_ADD_FAILED : ResultCodes.TEAM_USER_REMOVE_FAILED - throw new KeeperSdkError( - `Cannot ${operation} more than ${MAX_TEAM_USERS_PER_REQUEST} user-team pairs in one batch request.`, - code - ) - } + if (userCount > MAX_USERS_PER_OPERATION) { + throw new KeeperSdkError( + `Cannot process more than ${MAX_USERS_PER_OPERATION} users at once.`, + ResultCodes.NO_USERS_TO_UPDATE, + ); + } + if (teamCount > MAX_TEAMS_PER_OPERATION) { + throw new KeeperSdkError( + `Cannot process more than ${MAX_TEAMS_PER_OPERATION} teams at once.`, + ResultCodes.NO_TEAMS_FOR_USER_OP, + ); + } +} + +function validateBatchEntryCount( + count: number, + operation: "add" | "remove", +): void { + if (count > MAX_TEAM_USERS_PER_REQUEST) { + const code = + operation === "add" + ? ResultCodes.TEAM_USER_ADD_FAILED + : ResultCodes.TEAM_USER_REMOVE_FAILED; + throw new KeeperSdkError( + `Cannot ${operation} more than ${MAX_TEAM_USERS_PER_REQUEST} user-team pairs in one batch request.`, + code, + ); + } } async function prepareAddBatches( - teams: ResolvedTeam[], - users: EnterpriseUser[], - membership: Set, - publicKeyMap: Map, - treeKey: Uint8Array + teams: ResolvedTeam[], + users: EnterpriseUser[], + membership: Set, + publicKeyMap: Map, + treeKey: Uint8Array, ): Promise { - const items: TeamUserItemResult[] = [] - const batchTeams: PreparedBatchTeam[] = [] - const invitedQueue: InvitedQueueEntry[] = [] - const teamKeyCache = new Map() - - for (const team of teams) { - const prepared: PreparedBatchUser[] = [] - - for (const user of users) { - const base = buildItemBase(user, team) - - if (membership.has(membershipKey(user.enterprise_user_id, team.team_uid))) { - items.push({ ...base, status: TeamUserStatus.Skipped, skipReason: TeamUserSkipReason.AlreadyMember }) - continue - } - - if (user.status !== EnterpriseUserStatus.Active) { - invitedQueue.push({ user, team }) - continue - } - - const publicKeys = publicKeyMap.get(user.username.toLowerCase()) - if (!hasUsablePublicKey(publicKeys)) { - items.push({ - ...base, - status: TeamUserStatus.MissingPublicKey, - message: pickPublicKeyError(publicKeys, user.username), - }) - continue - } - - const teamKey = await getDecryptedTeamKey(team, treeKey, teamKeyCache) - if (!teamKey) { - items.push({ - ...base, - status: TeamUserStatus.Failed, - message: `Team key for "${team.name}" is unavailable.`, - }) - continue - } - - try { - prepared.push({ user, encryptedKey: await encryptTeamKeyForUser(teamKey, publicKeys) }) - } catch (err) { - items.push({ ...base, status: TeamUserStatus.Failed, message: extractErrorMessage(err) }) - } - } - - if (prepared.length > 0) batchTeams.push({ team, prepared }) + const items: TeamUserItemResult[] = []; + const batchTeams: PreparedBatchTeam[] = []; + const invitedQueue: InvitedQueueEntry[] = []; + const teamKeyCache = new Map(); + + for (const team of teams) { + const prepared: PreparedBatchUser[] = []; + + for (const user of users) { + const base = buildItemBase(user, team); + + if ( + membership.has(membershipKey(user.enterprise_user_id, team.team_uid)) + ) { + items.push({ + ...base, + status: TeamUserStatus.Skipped, + skipReason: TeamUserSkipReason.AlreadyMember, + }); + continue; + } + + if (user.status !== EnterpriseUserStatus.Active) { + invitedQueue.push({ user, team }); + continue; + } + + const publicKeys = publicKeyMap.get(user.username.toLowerCase()); + if (!hasUsablePublicKey(publicKeys)) { + items.push({ + ...base, + status: TeamUserStatus.MissingPublicKey, + message: pickPublicKeyError(publicKeys, user.username), + }); + continue; + } + + const teamKey = await getDecryptedTeamKey(team, treeKey, teamKeyCache); + if (!teamKey) { + items.push({ + ...base, + status: TeamUserStatus.Failed, + message: `Team key for "${team.name}" is unavailable.`, + }); + continue; + } + + try { + prepared.push({ + user, + encryptedKey: await encryptTeamKeyForUser(teamKey, publicKeys), + }); + } catch (err) { + items.push({ + ...base, + status: TeamUserStatus.Failed, + message: extractErrorMessage(err), + }); + } } - return { items, batchTeams, invitedQueue } + if (prepared.length > 0) batchTeams.push({ team, prepared }); + } + + return { items, batchTeams, invitedQueue }; } function buildAddTeamRequests( - batchTeams: PreparedBatchTeam[], - userType: Enterprise.TeamUserType | undefined + batchTeams: PreparedBatchTeam[], + userType: Enterprise.TeamUserType | undefined, ): Enterprise.ITeamsEnterpriseUsersAddTeamRequest[] { - return batchTeams.map(({ team, prepared }) => ({ - teamUid: normal64Bytes(team.team_uid), - users: prepared.map(({ user, encryptedKey }) => { - const userReq: Enterprise.ITeamsEnterpriseUsersAddUserRequest = { - enterpriseUserId: user.enterprise_user_id, - typedTeamKey: { key: encryptedKey.key, keyType: encryptedKey.keyType }, - } - if (userType !== undefined) userReq.userType = userType - return userReq - }), - })) -} - -function markAllBatchUsersFailed(batchTeams: PreparedBatchTeam[], message: string): TeamUserItemResult[] { - return batchTeams.flatMap(({ team, prepared }) => - prepared.map(({ user }) => ({ ...buildItemBase(user, team), status: TeamUserStatus.Failed, message })) - ) + return batchTeams.map(({ team, prepared }) => ({ + teamUid: normal64Bytes(team.team_uid), + users: prepared.map(({ user, encryptedKey }) => { + const userReq: Enterprise.ITeamsEnterpriseUsersAddUserRequest = { + enterpriseUserId: user.enterprise_user_id, + typedTeamKey: { key: encryptedKey.key, keyType: encryptedKey.keyType }, + }; + if (userType !== undefined) userReq.userType = userType; + return userReq; + }), + })); +} + +function markAllBatchUsersFailed( + batchTeams: PreparedBatchTeam[], + message: string, +): TeamUserItemResult[] { + return batchTeams.flatMap(({ team, prepared }) => + prepared.map(({ user }) => ({ + ...buildItemBase(user, team), + status: TeamUserStatus.Failed, + message, + })), + ); } function mergeTeamResponse( - prepTeam: PreparedBatchTeam, - teamResp: Enterprise.ITeamsEnterpriseUsersAddTeamResponse + prepTeam: PreparedBatchTeam, + teamResp: Enterprise.ITeamsEnterpriseUsersAddTeamResponse, ): TeamUserItemResult[] { - const userMap = new Map(prepTeam.prepared.map((p) => [p.user.enterprise_user_id, p.user])) - const teamFailureMessage = - teamResp.success === false - ? pickResponseError(teamResp.message, teamResp.resultCode, teamResp.additionalInfo, 'Team add failed') - : undefined - - const items: TeamUserItemResult[] = [] - for (const userResp of teamResp.users || []) { - const enterpriseUserId = toEnterpriseUserId(userResp.enterpriseUserId) - if (enterpriseUserId === null) continue - const user = userMap.get(enterpriseUserId) - if (!user) continue - const success = userResp.success === true - items.push({ - ...buildItemBase(user, prepTeam.team), - status: success ? TeamUserStatus.Added : TeamUserStatus.Failed, - message: success - ? undefined - : pickResponseError( - userResp.message, - userResp.resultCode, - userResp.additionalInfo, - teamFailureMessage - ), - }) - userMap.delete(enterpriseUserId) - } - - for (const user of userMap.values()) { - items.push({ - ...buildItemBase(user, prepTeam.team), - status: teamFailureMessage ? TeamUserStatus.Failed : TeamUserStatus.Added, - message: teamFailureMessage, - }) - } - return items + const userMap = new Map( + prepTeam.prepared.map((p) => [p.user.enterprise_user_id, p.user]), + ); + const teamFailureMessage = + teamResp.success === false + ? pickResponseError( + teamResp.message, + teamResp.resultCode, + teamResp.additionalInfo, + "Team add failed", + ) + : undefined; + + const items: TeamUserItemResult[] = []; + for (const userResp of teamResp.users || []) { + const enterpriseUserId = toEnterpriseUserId(userResp.enterpriseUserId); + if (enterpriseUserId === null) continue; + const user = userMap.get(enterpriseUserId); + if (!user) continue; + const success = userResp.success === true; + items.push({ + ...buildItemBase(user, prepTeam.team), + status: success ? TeamUserStatus.Added : TeamUserStatus.Failed, + message: success + ? undefined + : pickResponseError( + userResp.message, + userResp.resultCode, + userResp.additionalInfo, + teamFailureMessage, + ), + }); + userMap.delete(enterpriseUserId); + } + + for (const user of userMap.values()) { + items.push({ + ...buildItemBase(user, prepTeam.team), + status: teamFailureMessage ? TeamUserStatus.Failed : TeamUserStatus.Added, + message: teamFailureMessage, + }); + } + return items; } async function sendTeamsEnterpriseUsersAdd( - auth: Auth, - batchTeams: PreparedBatchTeam[], - userType: Enterprise.TeamUserType | undefined + auth: Auth, + batchTeams: PreparedBatchTeam[], + userType: Enterprise.TeamUserType | undefined, ): Promise { - if (batchTeams.length === 0) return [] - - validateBatchEntryCount( - batchTeams.reduce((count, batch) => count + batch.prepared.length, 0), - 'add' - ) - - let response: Enterprise.ITeamsEnterpriseUsersAddResponse - try { - response = await auth.executeRest( - teamsEnterpriseUsersAdd({ teams: buildAddTeamRequests(batchTeams, userType) }) - ) - } catch (err) { - return markAllBatchUsersFailed(batchTeams, extractErrorMessage(err)) - } - - const teamResponses = response.teams ?? [] - if (teamResponses.length === 0) { - return markAllBatchUsersFailed(batchTeams, UNEXPECTED_TEAM_RESPONSE_MESSAGE) - } - - const items: TeamUserItemResult[] = [] - const teamMap = new Map(batchTeams.map((p) => [p.team.team_uid, p])) - - for (const teamResp of teamResponses) { - const teamUid = teamResp.teamUid ? webSafe64FromBytes(teamResp.teamUid as Uint8Array) : '' - const prepTeam = teamMap.get(teamUid) - if (!prepTeam) continue - items.push(...mergeTeamResponse(prepTeam, teamResp)) - teamMap.delete(teamUid) - } - - for (const prepTeam of teamMap.values()) { - for (const { user } of prepTeam.prepared) { - items.push({ - ...buildItemBase(user, prepTeam.team), - status: TeamUserStatus.Failed, - message: MISSING_TEAM_RESPONSE_MESSAGE, - }) - } + if (batchTeams.length === 0) return []; + + validateBatchEntryCount( + batchTeams.reduce((count, batch) => count + batch.prepared.length, 0), + "add", + ); + + let response: Enterprise.ITeamsEnterpriseUsersAddResponse; + try { + response = await auth.executeRest( + teamsEnterpriseUsersAdd({ + teams: buildAddTeamRequests(batchTeams, userType), + }), + ); + } catch (err) { + return markAllBatchUsersFailed(batchTeams, extractErrorMessage(err)); + } + + const teamResponses = response.teams ?? []; + if (teamResponses.length === 0) { + return markAllBatchUsersFailed( + batchTeams, + UNEXPECTED_TEAM_RESPONSE_MESSAGE, + ); + } + + const items: TeamUserItemResult[] = []; + const teamMap = new Map(batchTeams.map((p) => [p.team.team_uid, p])); + + for (const teamResp of teamResponses) { + const teamUid = teamResp.teamUid + ? webSafe64FromBytes(teamResp.teamUid as Uint8Array) + : ""; + const prepTeam = teamMap.get(teamUid); + if (!prepTeam) continue; + items.push(...mergeTeamResponse(prepTeam, teamResp)); + teamMap.delete(teamUid); + } + + for (const prepTeam of teamMap.values()) { + for (const { user } of prepTeam.prepared) { + items.push({ + ...buildItemBase(user, prepTeam.team), + status: TeamUserStatus.Failed, + message: MISSING_TEAM_RESPONSE_MESSAGE, + }); } - return items + } + return items; } async function executeTeamUserCommand( - auth: Auth, - command: RestCommand<{ enterprise_user_id: number; team_uid: string }, KeeperResponse>, - fallbackErrorCode: string + auth: Auth, + command: RestCommand< + { enterprise_user_id: number; team_uid: string }, + KeeperResponse + >, + fallbackErrorCode: string, ): Promise { - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== SUCCESS_RESULT) { - const { enterprise_user_id: userId, team_uid: teamUid } = command.request - throw new KeeperSdkError( - response.message || - response.result_code || - `${command.baseRequest.command} failed for user=${userId}, team=${teamUid}`, - response.result_code || fallbackErrorCode - ) - } + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== SUCCESS_RESULT) { + const { enterprise_user_id: userId, team_uid: teamUid } = command.request; + throw new KeeperSdkError( + response.message || + response.result_code || + `${command.baseRequest.command} failed for user=${userId}, team=${teamUid}`, + response.result_code || fallbackErrorCode, + ); + } } function prepareRemoveEntries( - teams: ResolvedTeam[], - users: EnterpriseUser[], - membership: Set + teams: ResolvedTeam[], + users: EnterpriseUser[], + membership: Set, ): RemoveBatchPreparation { - const items: TeamUserItemResult[] = [] - const toRemove: PreparedRemoveEntry[] = [] - - for (const team of teams) { - for (const user of users) { - const base = buildItemBase(user, team) - if (!membership.has(membershipKey(user.enterprise_user_id, team.team_uid))) { - items.push({ ...base, status: TeamUserStatus.Skipped, skipReason: TeamUserSkipReason.NotMember }) - continue - } - toRemove.push({ user, team }) - } + const items: TeamUserItemResult[] = []; + const toRemove: PreparedRemoveEntry[] = []; + + for (const team of teams) { + for (const user of users) { + const base = buildItemBase(user, team); + if ( + !membership.has(membershipKey(user.enterprise_user_id, team.team_uid)) + ) { + items.push({ + ...base, + status: TeamUserStatus.Skipped, + skipReason: TeamUserSkipReason.NotMember, + }); + continue; + } + toRemove.push({ user, team }); } + } - return { items, toRemove } + return { items, toRemove }; } -function markAllRemoveFailed(entries: PreparedRemoveEntry[], message: string): TeamUserItemResult[] { - return entries.map(({ user, team }) => ({ - ...buildItemBase(user, team), - status: TeamUserStatus.Failed, - message, - })) +function markAllRemoveFailed( + entries: PreparedRemoveEntry[], + message: string, +): TeamUserItemResult[] { + return entries.map(({ user, team }) => ({ + ...buildItemBase(user, team), + status: TeamUserStatus.Failed, + message, + })); } async function sendTeamsEnterpriseUsersRemove( - auth: Auth, - entries: PreparedRemoveEntry[] + auth: Auth, + entries: PreparedRemoveEntry[], ): Promise { - if (entries.length === 0) return [] - - validateBatchEntryCount(entries.length, 'remove') - - const request: Enterprise.ITeamEnterpriseUserRemovesRequest = { - teamEnterpriseUserRemove: entries.map(({ user, team }) => ({ - teamUid: normal64Bytes(team.team_uid), - enterpriseUserId: user.enterprise_user_id, - })), - } - - let response: Enterprise.ITeamEnterpriseUserRemovesResponse + if (entries.length === 0) return []; + + validateBatchEntryCount(entries.length, "remove"); + + const request: Enterprise.ITeamEnterpriseUserRemovesRequest = { + teamEnterpriseUserRemove: entries.map(({ user, team }) => ({ + teamUid: normal64Bytes(team.team_uid), + enterpriseUserId: user.enterprise_user_id, + })), + }; + + let response: Enterprise.ITeamEnterpriseUserRemovesResponse; + try { + response = await auth.executeRest(teamsEnterpriseUsersRemove(request)); + } catch (err) { + return markAllRemoveFailed(entries, extractErrorMessage(err)); + } + + const removeResponses = response.teamEnterpriseUserRemoveResponse ?? []; + if (removeResponses.length === 0) { + return markAllRemoveFailed(entries, UNEXPECTED_REMOVE_RESPONSE_MESSAGE); + } + + const entryMap = new Map( + entries.map((entry) => [ + membershipKey(entry.user.enterprise_user_id, entry.team.team_uid), + entry, + ]), + ); + const items: TeamUserItemResult[] = []; + + for (const userResp of removeResponses) { + const remove = userResp.teamEnterpriseUserRemove; + const teamUid = remove?.teamUid + ? webSafe64FromBytes(remove.teamUid as Uint8Array) + : ""; + const enterpriseUserId = toEnterpriseUserId(remove?.enterpriseUserId); + if (!teamUid || enterpriseUserId === null) continue; + + const entry = entryMap.get(membershipKey(enterpriseUserId, teamUid)); + if (!entry) continue; + + const success = userResp.success === true; + items.push({ + ...buildItemBase(entry.user, entry.team), + status: success ? TeamUserStatus.Removed : TeamUserStatus.Failed, + message: success + ? undefined + : pickResponseError( + userResp.message, + userResp.resultCode, + userResp.additionalInfo, + "Team remove failed", + ), + }); + entryMap.delete(membershipKey(enterpriseUserId, teamUid)); + } + + for (const entry of entryMap.values()) { + items.push({ + ...buildItemBase(entry.user, entry.team), + status: TeamUserStatus.Failed, + message: MISSING_REMOVE_RESPONSE_MESSAGE, + }); + } + + return items; +} + +async function processInvitedQueue( + auth: Auth, + invitedQueue: InvitedQueueEntry[], +): Promise { + const items: TeamUserItemResult[] = []; + for (const { user, team } of invitedQueue) { + const base = buildItemBase(user, team); try { - response = await auth.executeRest(teamsEnterpriseUsersRemove(request)) + await executeTeamUserCommand( + auth, + teamQueueUserCommand({ + enterprise_user_id: user.enterprise_user_id, + team_uid: team.team_uid, + }), + ResultCodes.TEAM_USER_ADD_FAILED, + ); + items.push({ ...base, status: TeamUserStatus.Added }); } catch (err) { - return markAllRemoveFailed(entries, extractErrorMessage(err)) - } - - const removeResponses = response.teamEnterpriseUserRemoveResponse ?? [] - if (removeResponses.length === 0) { - return markAllRemoveFailed(entries, UNEXPECTED_REMOVE_RESPONSE_MESSAGE) - } - - const entryMap = new Map( - entries.map((entry) => [membershipKey(entry.user.enterprise_user_id, entry.team.team_uid), entry]) - ) - const items: TeamUserItemResult[] = [] - - for (const userResp of removeResponses) { - const remove = userResp.teamEnterpriseUserRemove - const teamUid = remove?.teamUid ? webSafe64FromBytes(remove.teamUid as Uint8Array) : '' - const enterpriseUserId = toEnterpriseUserId(remove?.enterpriseUserId) - if (!teamUid || enterpriseUserId === null) continue - - const entry = entryMap.get(membershipKey(enterpriseUserId, teamUid)) - if (!entry) continue - - const success = userResp.success === true - items.push({ - ...buildItemBase(entry.user, entry.team), - status: success ? TeamUserStatus.Removed : TeamUserStatus.Failed, - message: success - ? undefined - : pickResponseError(userResp.message, userResp.resultCode, userResp.additionalInfo, 'Team remove failed'), - }) - entryMap.delete(membershipKey(enterpriseUserId, teamUid)) - } - - for (const entry of entryMap.values()) { - items.push({ - ...buildItemBase(entry.user, entry.team), - status: TeamUserStatus.Failed, - message: MISSING_REMOVE_RESPONSE_MESSAGE, - }) - } - - return items -} - -async function processInvitedQueue(auth: Auth, invitedQueue: InvitedQueueEntry[]): Promise { - const items: TeamUserItemResult[] = [] - for (const { user, team } of invitedQueue) { - const base = buildItemBase(user, team) - try { - await executeTeamUserCommand( - auth, - teamQueueUserCommand({ enterprise_user_id: user.enterprise_user_id, team_uid: team.team_uid }), - ResultCodes.TEAM_USER_ADD_FAILED - ) - items.push({ ...base, status: TeamUserStatus.Added }) - } catch (err) { - items.push({ ...base, status: TeamUserStatus.Failed, message: extractErrorMessage(err) }) - } + items.push({ + ...base, + status: TeamUserStatus.Failed, + message: extractErrorMessage(err), + }); } - return items + } + return items; } -export async function addUsersToTeams(auth: Auth, input: AddUsersToTeamsInput): Promise { - const ctx = await loadTeamUserContext(auth, input.users, input.teams || []) - - const treeKey = await ctx.enterpriseData.getTreeKey() - if (!treeKey) { - throw new KeeperSdkError('Enterprise tree key is unavailable.', ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE) - } - - const publicKeyMap = await fetchUserPublicKeys( +export async function addUsersToTeams( + auth: Auth, + input: AddUsersToTeamsInput, +): Promise { + const ctx = await loadTeamUserContext(auth, input.users, input.teams || []); + + const treeKey = await ctx.enterpriseData.getTreeKey(); + if (!treeKey) { + throw new KeeperSdkError( + "Enterprise tree key is unavailable.", + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE, + ); + } + + const publicKeyMap = await fetchUserPublicKeys( + auth, + ctx.users + .filter((u) => u.status === EnterpriseUserStatus.Active) + .map((u) => u.username), + ); + + const { items, batchTeams, invitedQueue } = await prepareAddBatches( + ctx.teams, + ctx.users, + ctx.membership, + publicKeyMap, + treeKey, + ); + + if (batchTeams.length > 0) { + items.push( + ...(await sendTeamsEnterpriseUsersAdd( auth, - ctx.users.filter((u) => u.status === EnterpriseUserStatus.Active).map((u) => u.username) - ) - - const { items, batchTeams, invitedQueue } = await prepareAddBatches( - ctx.teams, - ctx.users, - ctx.membership, - publicKeyMap, - treeKey - ) - - if (batchTeams.length > 0) { - items.push(...(await sendTeamsEnterpriseUsersAdd(auth, batchTeams, determineUserType(input.hideSharedFolders)))) - } - items.push(...(await processInvitedQueue(auth, invitedQueue))) + batchTeams, + determineUserType(input.hideSharedFolders), + )), + ); + } + items.push(...(await processInvitedQueue(auth, invitedQueue))); - return finalizeResult(items) + return finalizeResult(items); } export async function removeUsersFromTeams( - auth: Auth, - input: RemoveUsersFromTeamsInput + auth: Auth, + input: RemoveUsersFromTeamsInput, ): Promise { - const ctx = await loadTeamUserContext(auth, input.users, input.teams || []) - const { items, toRemove } = prepareRemoveEntries(ctx.teams, ctx.users, ctx.membership) + const ctx = await loadTeamUserContext(auth, input.users, input.teams || []); + const { items, toRemove } = prepareRemoveEntries( + ctx.teams, + ctx.users, + ctx.membership, + ); - if (toRemove.length > 0) { - items.push(...(await sendTeamsEnterpriseUsersRemove(auth, toRemove))) - } + if (toRemove.length > 0) { + items.push(...(await sendTeamsEnterpriseUsersRemove(auth, toRemove))); + } - return finalizeResult(items) + return finalizeResult(items); } function finalizeResult(items: TeamUserItemResult[]): TeamUserResult { - let succeeded = 0 - let skipped = 0 - let failed = 0 - for (const item of items) { - if (item.status === TeamUserStatus.Added || item.status === TeamUserStatus.Removed) succeeded++ - else if (item.status === TeamUserStatus.Skipped) skipped++ - else failed++ - } - return { success: failed === 0 && succeeded > 0, items, succeeded, skipped, failed } -} - -export function formatTeamUserResult(result: TeamUserResult): FormattedTeamUserTable { - const rows = result.items.map((item, index) => [ - String(index + 1), - item.status, - item.username, - String(item.enterpriseUserId), - item.teamName, - item.teamUid, - item.message || item.skipReason || '', - ]) - return { - headers: [...TEAM_USER_TABLE_HEADERS], - rows, - summary: `Succeeded: ${result.succeeded} Skipped: ${result.skipped} Failed: ${result.failed}`, - } -} - -export function renderTeamUserAsciiTable(table: FormattedTeamUserTable): string { - const { headers, rows } = table - const widths = headers.map((header, columnIndex) => - Math.max(header.length, ...rows.map((row) => (row[columnIndex] || '').length)) + let succeeded = 0; + let skipped = 0; + let failed = 0; + for (const item of items) { + if ( + item.status === TeamUserStatus.Added || + item.status === TeamUserStatus.Removed ) - const padCell = (cell: string, columnIndex: number): string => - cell + ' '.repeat(Math.max(0, widths[columnIndex] - cell.length)) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - return [ - formatRow(headers), - formatRow(widths.map((w) => '-'.repeat(w))), - ...rows.map(formatRow), - table.summary, - ].join('\n') + succeeded++; + else if (item.status === TeamUserStatus.Skipped) skipped++; + else failed++; + } + return { + success: failed === 0 && succeeded > 0, + items, + succeeded, + skipped, + failed, + }; +} + +export function formatTeamUserResult( + result: TeamUserResult, +): FormattedTeamUserTable { + const rows = result.items.map((item, index) => [ + String(index + 1), + item.status, + item.username, + String(item.enterpriseUserId), + item.teamName, + item.teamUid, + item.message || item.skipReason || "", + ]); + return { + headers: [...TEAM_USER_TABLE_HEADERS], + rows, + summary: `Succeeded: ${result.succeeded} Skipped: ${result.skipped} Failed: ${result.failed}`, + }; +} + +export function renderTeamUserAsciiTable( + table: FormattedTeamUserTable, +): string { + const { headers, rows } = table; + const widths = headers.map((header, columnIndex) => + Math.max( + header.length, + ...rows.map((row) => (row[columnIndex] || "").length), + ), + ); + const padCell = (cell: string, columnIndex: number): string => + cell + " ".repeat(Math.max(0, widths[columnIndex] - cell.length)); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + return [ + formatRow(headers), + formatRow(widths.map((w) => "-".repeat(w))), + ...rows.map(formatRow), + table.summary, + ].join("\n"); } diff --git a/KeeperSdk/src/users/updateUser.ts b/KeeperSdk/src/users/updateUser.ts index feb1932a..6ec7cbc9 100644 --- a/KeeperSdk/src/users/updateUser.ts +++ b/KeeperSdk/src/users/updateUser.ts @@ -1,330 +1,390 @@ import { - encryptObjectForStorage, - type Auth, - type KeeperResponse, - type RestCommand, - teamEnterpriseUserRemoveCommand, -} from '@keeper-security/keeperapi' -import { extractErrorMessage, isNumber, KeeperSdkError, ResultCodes } from '../utils' + encryptObjectForStorage, + type Auth, + type KeeperResponse, + type RestCommand, + teamEnterpriseUserRemoveCommand, +} from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseRole, - type EnterpriseTeamRecord, -} from '../teams/enterpriseData' + extractErrorMessage, + isNumber, + KeeperSdkError, + ResultCodes, +} from "../utils"; import { - applyDecryptedNodeNames, - applyEnterpriseNameToRoot, - parentNeedsNameLookup, - resolveParentNode, -} from '../teams/teamUtils' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseRole, + type EnterpriseTeamRecord, +} from "../teams/enterpriseData"; import { - normalizeEmailInputs, - validateUserProfileFields, - resolveExistingUsers, - UpdateUserStatus, - type UpdateUserInput, - type UpdateUserItemResult, - type UpdateUserResult, - type FormattedUpdateUserTable, -} from './userTypes' - -export { UpdateUserStatus } -export type { UpdateUserInput, UpdateUserItemResult, UpdateUserResult, FormattedUpdateUserTable } - -const USER_UPDATE_COMMAND = 'enterprise_user_update' -const ROLE_USER_REMOVE_COMMAND = 'role_user_remove' + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveParentNode, +} from "../teams/teamUtils"; +import { + normalizeEmailInputs, + validateUserProfileFields, + resolveExistingUsers, + UpdateUserStatus, + type UpdateUserInput, + type UpdateUserItemResult, + type UpdateUserResult, + type FormattedUpdateUserTable, +} from "./userTypes"; + +export { UpdateUserStatus }; +export type { + UpdateUserInput, + UpdateUserItemResult, + UpdateUserResult, + FormattedUpdateUserTable, +}; + +const USER_UPDATE_COMMAND = "enterprise_user_update"; +const ROLE_USER_REMOVE_COMMAND = "role_user_remove"; const UPDATE_USER_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Users, - EnterpriseDataInclude.Teams, - EnterpriseDataInclude.QueuedTeams, - EnterpriseDataInclude.TeamUsers, - EnterpriseDataInclude.QueuedTeamUsers, - EnterpriseDataInclude.Roles, - EnterpriseDataInclude.RoleUsers, -] - -const USER_UPDATE_TABLE_HEADERS = ['#', 'Status', 'Email', 'User ID', 'Node ID', 'Detail'] + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Users, + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.QueuedTeams, + EnterpriseDataInclude.TeamUsers, + EnterpriseDataInclude.QueuedTeamUsers, + EnterpriseDataInclude.Roles, + EnterpriseDataInclude.RoleUsers, +]; + +const USER_UPDATE_TABLE_HEADERS = [ + "#", + "Status", + "Email", + "User ID", + "Node ID", + "Detail", +]; type UserUpdatePayload = { - enterprise_user_id: number - enterprise_user_username: string - node_id: number - encrypted_data?: string - full_name?: string - job_title?: string -} + enterprise_user_id: number; + enterprise_user_username: string; + node_id: number; + encrypted_data?: string; + full_name?: string; + job_title?: string; +}; type RoleUserRemovePayload = { - role_id: number - enterprise_user_id: number -} - -export async function updateUsers(auth: Auth, input: UpdateUserInput): Promise { - const rawEmails = normalizeEmailInputs(input.emails) - - if (rawEmails.length === 0) { - throw new KeeperSdkError('No users provided for update.', ResultCodes.NO_USERS_TO_UPDATE) - } - - const parentIdentifier = input.parent ?? null - const needsNameLookup = parentNeedsNameLookup(parentIdentifier) - const hasProfileChange = parentIdentifier !== null || !!input.fullName || !!input.jobTitle - - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(UPDATE_USER_INCLUDES), - enterpriseData.getDisplayNames(), - ]) - - const nodes = response.nodes || [] - applyEnterpriseNameToRoot(nodes, response.enterprise_name) - - if (needsNameLookup) { - applyDecryptedNodeNames(nodes, displayNames.nodes) - await enterpriseData.decryptNodeNames(nodes) - } - - const resolvedUsers = resolveExistingUsers(response.users || [], rawEmails) - - const overrideNodeId: number | null = - parentIdentifier !== null && parentIdentifier !== '' - ? resolveParentNode(nodes, parentIdentifier).node_id - : null - - const treeKey = hasProfileChange ? await enterpriseData.getTreeKey() : null - if (hasProfileChange && !treeKey) { - throw new KeeperSdkError( - 'Enterprise tree key is unavailable.', - ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE - ) + role_id: number; + enterprise_user_id: number; +}; + +export async function updateUsers( + auth: Auth, + input: UpdateUserInput, +): Promise { + const rawEmails = normalizeEmailInputs(input.emails); + + if (rawEmails.length === 0) { + throw new KeeperSdkError( + "No users provided for update.", + ResultCodes.NO_USERS_TO_UPDATE, + ); + } + + const parentIdentifier = input.parent ?? null; + const needsNameLookup = parentNeedsNameLookup(parentIdentifier); + const hasProfileChange = + parentIdentifier !== null || !!input.fullName || !!input.jobTitle; + + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(UPDATE_USER_INCLUDES), + enterpriseData.getDisplayNames(), + ]); + + const nodes = response.nodes || []; + applyEnterpriseNameToRoot(nodes, response.enterprise_name); + + if (needsNameLookup) { + applyDecryptedNodeNames(nodes, displayNames.nodes); + await enterpriseData.decryptNodeNames(nodes); + } + + const resolvedUsers = resolveExistingUsers(response.users || [], rawEmails); + + const overrideNodeId: number | null = + parentIdentifier !== null && parentIdentifier !== "" + ? resolveParentNode(nodes, parentIdentifier).node_id + : null; + + const treeKey = hasProfileChange ? await enterpriseData.getTreeKey() : null; + if (hasProfileChange && !treeKey) { + throw new KeeperSdkError( + "Enterprise tree key is unavailable.", + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE, + ); + } + + const removeTeamUids = resolveTeamUids( + input.removeTeam || [], + response.teams || [], + response.queued_teams || [], + ); + const removeRoleIds = resolveRoleIds( + input.removeRole || [], + response.roles || [], + displayNames.roles, + ); + + const fullName = (input.fullName || "").trim() || undefined; + const jobTitle = (input.jobTitle || "").trim() || undefined; + validateUserProfileFields(fullName, jobTitle); + + const items: UpdateUserItemResult[] = []; + + for (const user of resolvedUsers) { + const targetNodeId = overrideNodeId ?? user.node_id ?? 0; + const item: UpdateUserItemResult = { + username: user.username, + enterpriseUserId: user.enterprise_user_id, + nodeId: targetNodeId, + status: UpdateUserStatus.Failed, + }; + + try { + if (hasProfileChange && treeKey !== null) { + const encryptedData = + fullName !== undefined + ? await encryptObjectForStorage({ displayname: fullName }, treeKey) + : undefined; + await sendUserUpdate(auth, { + enterprise_user_id: user.enterprise_user_id, + enterprise_user_username: user.username, + node_id: targetNodeId, + encrypted_data: encryptedData, + full_name: fullName, + job_title: jobTitle, + }); + } + + for (const teamUid of removeTeamUids) { + await sendTeamUserRemove(auth, user.enterprise_user_id, teamUid); + } + + for (const roleId of removeRoleIds) { + await sendRoleUserRemove(auth, roleId, user.enterprise_user_id); + } + + item.status = UpdateUserStatus.Updated; + } catch (err) { + item.nodeId = user.node_id ?? 0; + item.message = extractErrorMessage(err); } + items.push(item); + } - const removeTeamUids = resolveTeamUids(input.removeTeam || [], response.teams || [], response.queued_teams || []) - const removeRoleIds = resolveRoleIds(input.removeRole || [], response.roles || [], displayNames.roles) - - const fullName = (input.fullName || '').trim() || undefined - const jobTitle = (input.jobTitle || '').trim() || undefined - validateUserProfileFields(fullName, jobTitle) - - const items: UpdateUserItemResult[] = [] - - for (const user of resolvedUsers) { - const targetNodeId = overrideNodeId ?? (user.node_id ?? 0) - const item: UpdateUserItemResult = { - username: user.username, - enterpriseUserId: user.enterprise_user_id, - nodeId: targetNodeId, - status: UpdateUserStatus.Failed, - } - - try { - if (hasProfileChange && treeKey !== null) { - const encryptedData = fullName !== undefined - ? await encryptObjectForStorage({ displayname: fullName }, treeKey) - : undefined - await sendUserUpdate(auth, { - enterprise_user_id: user.enterprise_user_id, - enterprise_user_username: user.username, - node_id: targetNodeId, - encrypted_data: encryptedData, - full_name: fullName, - job_title: jobTitle, - }) - } - - for (const teamUid of removeTeamUids) { - await sendTeamUserRemove(auth, user.enterprise_user_id, teamUid) - } - - for (const roleId of removeRoleIds) { - await sendRoleUserRemove(auth, roleId, user.enterprise_user_id) - } - - item.status = UpdateUserStatus.Updated - } catch (err) { - item.nodeId = user.node_id ?? 0 - item.message = extractErrorMessage(err) - } - items.push(item) - } - - return finalizeResult(items) + return finalizeResult(items); } -async function sendUserUpdate(auth: Auth, payload: UserUpdatePayload): Promise { - const command: RestCommand = { - baseRequest: { command: USER_UPDATE_COMMAND }, - request: payload, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `${USER_UPDATE_COMMAND} failed for "${payload.enterprise_user_username}"`, - response.result_code || ResultCodes.USER_UPDATE_FAILED - ) - } +async function sendUserUpdate( + auth: Auth, + payload: UserUpdatePayload, +): Promise { + const command: RestCommand = { + baseRequest: { command: USER_UPDATE_COMMAND }, + request: payload, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `${USER_UPDATE_COMMAND} failed for "${payload.enterprise_user_username}"`, + response.result_code || ResultCodes.USER_UPDATE_FAILED, + ); + } } -async function sendTeamUserRemove(auth: Auth, enterpriseUserId: number, teamUid: string): Promise { - const command = teamEnterpriseUserRemoveCommand({ - enterprise_user_id: enterpriseUserId, - team_uid: teamUid, - }) - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `team_enterprise_user_remove failed for user=${enterpriseUserId}, team=${teamUid}`, - response.result_code || ResultCodes.USER_UPDATE_FAILED - ) - } +async function sendTeamUserRemove( + auth: Auth, + enterpriseUserId: number, + teamUid: string, +): Promise { + const command = teamEnterpriseUserRemoveCommand({ + enterprise_user_id: enterpriseUserId, + team_uid: teamUid, + }); + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `team_enterprise_user_remove failed for user=${enterpriseUserId}, team=${teamUid}`, + response.result_code || ResultCodes.USER_UPDATE_FAILED, + ); + } } -async function sendRoleUserRemove(auth: Auth, roleId: number, enterpriseUserId: number): Promise { - const command: RestCommand = { - baseRequest: { command: ROLE_USER_REMOVE_COMMAND }, - request: { role_id: roleId, enterprise_user_id: enterpriseUserId }, - authorization: {}, - } - const response = await auth.executeRestCommand(command) - const result = (response.result || '').toLowerCase() - if (result && result !== 'success') { - throw new KeeperSdkError( - response.message || - response.result_code || - `${ROLE_USER_REMOVE_COMMAND} failed for user=${enterpriseUserId}, role=${roleId}`, - response.result_code || ResultCodes.USER_UPDATE_FAILED - ) - } +async function sendRoleUserRemove( + auth: Auth, + roleId: number, + enterpriseUserId: number, +): Promise { + const command: RestCommand = { + baseRequest: { command: ROLE_USER_REMOVE_COMMAND }, + request: { role_id: roleId, enterprise_user_id: enterpriseUserId }, + authorization: {}, + }; + const response = await auth.executeRestCommand(command); + const result = (response.result || "").toLowerCase(); + if (result && result !== "success") { + throw new KeeperSdkError( + response.message || + response.result_code || + `${ROLE_USER_REMOVE_COMMAND} failed for user=${enterpriseUserId}, role=${roleId}`, + response.result_code || ResultCodes.USER_UPDATE_FAILED, + ); + } } function resolveTeamUids( - identifiers: string[], - teams: EnterpriseTeamRecord[], - queuedTeams: EnterpriseTeamRecord[] + identifiers: string[], + teams: EnterpriseTeamRecord[], + queuedTeams: EnterpriseTeamRecord[], ): string[] { - if (identifiers.length === 0) return [] + if (identifiers.length === 0) return []; - const uidSet = new Set() - const byLowerName = new Map() + const uidSet = new Set(); + const byLowerName = new Map(); - for (const t of teams) { - if (t.team_uid) { - uidSet.add(t.team_uid) - const lower = (t.name || '').trim().toLowerCase() - if (lower && !byLowerName.has(lower)) byLowerName.set(lower, t.team_uid) - } + for (const t of teams) { + if (t.team_uid) { + uidSet.add(t.team_uid); + const lower = (t.name || "").trim().toLowerCase(); + if (lower && !byLowerName.has(lower)) byLowerName.set(lower, t.team_uid); } - for (const t of queuedTeams) { - if (t.team_uid) { - uidSet.add(t.team_uid) - const lower = (t.name || '').trim().toLowerCase() - if (lower && !byLowerName.has(lower)) byLowerName.set(lower, t.team_uid) - } + } + for (const t of queuedTeams) { + if (t.team_uid) { + uidSet.add(t.team_uid); + const lower = (t.name || "").trim().toLowerCase(); + if (lower && !byLowerName.has(lower)) byLowerName.set(lower, t.team_uid); } - - const seen = new Set() - const result: string[] = [] - for (const id of identifiers) { - const trimmed = id.trim() - const uid = (uidSet.has(trimmed) ? trimmed : undefined) ?? byLowerName.get(trimmed.toLowerCase()) - if (!uid) { - throw new KeeperSdkError(`Team "${trimmed}" does not exist.`, ResultCodes.TEAM_NOT_FOUND) - } - if (!seen.has(uid)) { - seen.add(uid) - result.push(uid) - } + } + + const seen = new Set(); + const result: string[] = []; + for (const id of identifiers) { + const trimmed = id.trim(); + const uid = + (uidSet.has(trimmed) ? trimmed : undefined) ?? + byLowerName.get(trimmed.toLowerCase()); + if (!uid) { + throw new KeeperSdkError( + `Team "${trimmed}" does not exist.`, + ResultCodes.TEAM_NOT_FOUND, + ); + } + if (!seen.has(uid)) { + seen.add(uid); + result.push(uid); } - return result + } + return result; } function resolveRoleIds( - identifiers: string[], - roles: EnterpriseRole[], - decryptedRoleNames: Map + identifiers: string[], + roles: EnterpriseRole[], + decryptedRoleNames: Map, ): number[] { - if (identifiers.length === 0) return [] - - const idSet = new Set() - const byLowerName = new Map() - - for (const r of roles) { - if (!isNumber(r.role_id)) continue - idSet.add(r.role_id) - const lower = (decryptedRoleNames.get(r.role_id) || '').trim().toLowerCase() - if (lower && !byLowerName.has(lower)) byLowerName.set(lower, r.role_id) + if (identifiers.length === 0) return []; + + const idSet = new Set(); + const byLowerName = new Map(); + + for (const r of roles) { + if (!isNumber(r.role_id)) continue; + idSet.add(r.role_id); + const lower = (decryptedRoleNames.get(r.role_id) || "") + .trim() + .toLowerCase(); + if (lower && !byLowerName.has(lower)) byLowerName.set(lower, r.role_id); + } + + const seen = new Set(); + const result: number[] = []; + for (const id of identifiers) { + const trimmed = id.trim(); + const numericId = Number(trimmed); + let roleId: number | undefined; + + if (Number.isInteger(numericId)) + roleId = idSet.has(numericId) ? numericId : undefined; + if (roleId === undefined) roleId = byLowerName.get(trimmed.toLowerCase()); + if (roleId === undefined) { + throw new KeeperSdkError( + `Role "${trimmed}" does not exist.`, + ResultCodes.ROLE_NOT_FOUND, + ); } - - const seen = new Set() - const result: number[] = [] - for (const id of identifiers) { - const trimmed = id.trim() - const numericId = Number(trimmed) - let roleId: number | undefined - - if (Number.isInteger(numericId)) roleId = idSet.has(numericId) ? numericId : undefined - if (roleId === undefined) roleId = byLowerName.get(trimmed.toLowerCase()) - if (roleId === undefined) { - throw new KeeperSdkError(`Role "${trimmed}" does not exist.`, ResultCodes.ROLE_NOT_FOUND) - } - if (!seen.has(roleId)) { - seen.add(roleId) - result.push(roleId) - } + if (!seen.has(roleId)) { + seen.add(roleId); + result.push(roleId); } - return result + } + return result; } function finalizeResult(items: UpdateUserItemResult[]): UpdateUserResult { - let updated = 0, failed = 0 - for (const item of items) { - if (item.status === UpdateUserStatus.Updated) updated++ - else failed++ - } - return { success: failed === 0 && updated > 0, items, updated, failed } + let updated = 0, + failed = 0; + for (const item of items) { + if (item.status === UpdateUserStatus.Updated) updated++; + else failed++; + } + return { success: failed === 0 && updated > 0, items, updated, failed }; } -export function formatUpdateUserResult(result: UpdateUserResult): FormattedUpdateUserTable { - const rows = result.items.map((item, index) => [ - String(index + 1), - item.status, - item.username, - String(item.enterpriseUserId), - String(item.nodeId), - item.message || '', - ]) - return { - headers: [...USER_UPDATE_TABLE_HEADERS], - rows, - summary: `Updated: ${result.updated} Failed: ${result.failed}`, - } +export function formatUpdateUserResult( + result: UpdateUserResult, +): FormattedUpdateUserTable { + const rows = result.items.map((item, index) => [ + String(index + 1), + item.status, + item.username, + String(item.enterpriseUserId), + String(item.nodeId), + item.message || "", + ]); + return { + headers: [...USER_UPDATE_TABLE_HEADERS], + rows, + summary: `Updated: ${result.updated} Failed: ${result.failed}`, + }; } -export function renderUpdateUserAsciiTable(table: FormattedUpdateUserTable): string { - const { headers, rows } = table - const widths = headers.map((header, index) => - Math.max(header.length, ...rows.map((row) => (row[index] || '').length)) - ) - const padCell = (cell: string, columnIndex: number): string => - cell.padEnd(widths[columnIndex]) - const formatRow = (cells: string[]): string => - cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') - - const lines: string[] = [ - formatRow(headers), - formatRow(widths.map((w) => '-'.repeat(w))), - ...rows.map(formatRow), - table.summary, - ] - return lines.join('\n') +export function renderUpdateUserAsciiTable( + table: FormattedUpdateUserTable, +): string { + const { headers, rows } = table; + const widths = headers.map((header, index) => + Math.max(header.length, ...rows.map((row) => (row[index] || "").length)), + ); + const padCell = (cell: string, columnIndex: number): string => + cell.padEnd(widths[columnIndex]); + const formatRow = (cells: string[]): string => + cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(" "); + + const lines: string[] = [ + formatRow(headers), + formatRow(widths.map((w) => "-".repeat(w))), + ...rows.map(formatRow), + table.summary, + ]; + return lines.join("\n"); } diff --git a/KeeperSdk/src/users/userTypes.ts b/KeeperSdk/src/users/userTypes.ts index 760486f9..47823c0d 100644 --- a/KeeperSdk/src/users/userTypes.ts +++ b/KeeperSdk/src/users/userTypes.ts @@ -1,395 +1,404 @@ -import { KeeperSdkError, ResultCodes } from '../utils' -import type { EnterpriseUser } from '../teams/enterpriseData' +import { KeeperSdkError, ResultCodes } from "../utils"; +import type { EnterpriseUser } from "../teams/enterpriseData"; -export const MAX_FULL_NAME_LENGTH = 255 -export const MAX_JOB_TITLE_LENGTH = 255 +export const MAX_FULL_NAME_LENGTH = 255; +export const MAX_JOB_TITLE_LENGTH = 255; export enum EnterpriseUserStatus { - Active = 'active', - Invited = 'invited', + Active = "active", + Invited = "invited", } export enum UserColumn { - Name = 'name', - Status = 'status', - TransferStatus = 'transfer_status', - Node = 'node', - TeamCount = 'team_count', - Teams = 'teams', - QueuedTeamCount = 'queued_team_count', - QueuedTeams = 'queued_teams', - RoleCount = 'role_count', - Roles = 'roles', - Alias = 'alias', - TwoFaEnabled = '2fa_enabled', + Name = "name", + Status = "status", + TransferStatus = "transfer_status", + Node = "node", + TeamCount = "team_count", + Teams = "teams", + QueuedTeamCount = "queued_team_count", + QueuedTeams = "queued_teams", + RoleCount = "role_count", + Roles = "roles", + Alias = "alias", + TwoFaEnabled = "2fa_enabled", } export enum AddUserStatus { - Added = 'added', - Reinvited = 'reinvited', - Skipped = 'skipped', - Failed = 'failed', + Added = "added", + Reinvited = "reinvited", + Skipped = "skipped", + Failed = "failed", } export enum AddUserSkipReason { - AlreadyExists = 'already_exists', - InvalidEmail = 'invalid_email', + AlreadyExists = "already_exists", + InvalidEmail = "invalid_email", } export enum UpdateUserStatus { - Updated = 'updated', - Failed = 'failed', + Updated = "updated", + Failed = "failed", } export enum DeleteUserStatus { - Deleted = 'deleted', - Failed = 'failed', + Deleted = "deleted", + Failed = "failed", } export enum UserAction { - Lock = 'lock', - Unlock = 'unlock', - ExpirePassword = 'expire_password', + Lock = "lock", + Unlock = "unlock", + ExpirePassword = "expire_password", } export enum UserActionStatus { - Success = 'success', - Skipped = 'skipped', - Failed = 'failed', + Success = "success", + Skipped = "skipped", + Failed = "failed", } export enum UserActionSkipReason { - Inactive = 'inactive', - Pending = 'pending', + Inactive = "inactive", + Pending = "pending", } -export type UserColumnInput = UserColumn | `${UserColumn}` +export type UserColumnInput = UserColumn | `${UserColumn}`; export type UserTeamInfo = { - team_uid: string - name: string -} + team_uid: string; + name: string; +}; export type UserRoleInfo = { - role_id: number - role_name: string -} + role_id: number; + role_name: string; +}; export type ListUsersOptions = { - pattern?: string | null - columns?: UserColumnInput[] | '*' | string | null -} + pattern?: string | null; + columns?: UserColumnInput[] | "*" | string | null; +}; export type ListUserRow = { - enterprise_user_id: number - username: string - name?: string - status?: string - transfer_status?: string - node?: string - team_count?: number - teams?: string[] - queued_team_count?: number - queued_teams?: string[] - role_count?: number - roles?: string[] - alias?: string[] - tfa_enabled?: boolean -} + enterprise_user_id: number; + username: string; + name?: string; + status?: string; + transfer_status?: string; + node?: string; + team_count?: number; + teams?: string[]; + queued_team_count?: number; + queued_teams?: string[]; + role_count?: number; + roles?: string[]; + alias?: string[]; + tfa_enabled?: boolean; +}; export type FormattedUsersTable = { - headers: string[] - rows: string[][] -} + headers: string[]; + rows: string[][]; +}; export type FormatUsersTableOptions = { - columns?: ListUsersOptions['columns'] -} + columns?: ListUsersOptions["columns"]; +}; export type UserView = { - enterprise_user_id: number - username: string - node_id: number - node_name: string - full_name: string - status: string - tfa_enabled: boolean - transfer_status: string - aliases?: string[] - teams?: UserTeamInfo[] - queued_teams?: UserTeamInfo[] - roles?: UserRoleInfo[] - share_admins?: string[] -} + enterprise_user_id: number; + username: string; + node_id: number; + node_name: string; + full_name: string; + status: string; + tfa_enabled: boolean; + transfer_status: string; + aliases?: string[]; + teams?: UserTeamInfo[]; + queued_teams?: UserTeamInfo[]; + roles?: UserRoleInfo[]; + share_admins?: string[]; +}; export type FormatUserViewOptions = { - verbose?: boolean -} + verbose?: boolean; +}; export type UserViewTableRow = { - label: string - value: string | string[] - id?: string | string[] -} + label: string; + value: string | string[]; + id?: string | string[]; +}; export type FormattedUserViewTable = { - rows: UserViewTableRow[] - hasIdColumn: boolean -} + rows: UserViewTableRow[]; + hasIdColumn: boolean; +}; export type AddUserInput = { - emails: string[] - parent?: string | number | null - fullName?: string - jobTitle?: string -} + emails: string[]; + parent?: string | number | null; + fullName?: string; + jobTitle?: string; +}; export type AddUserItemResult = { - username: string - enterpriseUserId?: number - nodeId?: number - status: AddUserStatus - skipReason?: AddUserSkipReason - message?: string -} + username: string; + enterpriseUserId?: number; + nodeId?: number; + status: AddUserStatus; + skipReason?: AddUserSkipReason; + message?: string; +}; export type AddUserResult = { - success: boolean - parentNodeId: number - parentNodeName: string - items: AddUserItemResult[] - added: number - reinvited: number - skipped: number - failed: number -} + success: boolean; + parentNodeId: number; + parentNodeName: string; + items: AddUserItemResult[]; + added: number; + reinvited: number; + skipped: number; + failed: number; +}; export type FormatAddUserResultOptions = { - showSkipped?: boolean -} + showSkipped?: boolean; +}; export type FormattedAddUserTable = { - headers: string[] - rows: string[][] - parentNodeName: string - summary: string -} + headers: string[]; + rows: string[][]; + parentNodeName: string; + summary: string; +}; export type UpdateUserInput = { - emails: string[] - parent?: string | number | null - fullName?: string - jobTitle?: string - removeTeam?: string[] - removeRole?: string[] -} + emails: string[]; + parent?: string | number | null; + fullName?: string; + jobTitle?: string; + removeTeam?: string[]; + removeRole?: string[]; +}; export type UpdateUserItemResult = { - username: string - enterpriseUserId: number - nodeId: number - status: UpdateUserStatus - message?: string -} + username: string; + enterpriseUserId: number; + nodeId: number; + status: UpdateUserStatus; + message?: string; +}; export type UpdateUserResult = { - success: boolean - items: UpdateUserItemResult[] - updated: number - failed: number -} + success: boolean; + items: UpdateUserItemResult[]; + updated: number; + failed: number; +}; export type FormattedUpdateUserTable = { - headers: string[] - rows: string[][] - summary: string -} + headers: string[]; + rows: string[][]; + summary: string; +}; export type DeleteUserInput = { - emails: string[] -} + emails: string[]; +}; export type DeleteUserItemResult = { - username: string - enterpriseUserId: number - status: DeleteUserStatus - message?: string -} + username: string; + enterpriseUserId: number; + status: DeleteUserStatus; + message?: string; +}; export type DeleteUserResult = { - success: boolean - items: DeleteUserItemResult[] - deleted: number - failed: number -} + success: boolean; + items: DeleteUserItemResult[]; + deleted: number; + failed: number; +}; export type FormattedDeleteUserTable = { - headers: string[] - rows: string[][] - summary: string -} + headers: string[]; + rows: string[][]; + summary: string; +}; export type UserActionInput = { - emails: string[] - action: UserAction -} + emails: string[]; + action: UserAction; +}; export type UserActionItemResult = { - username: string - enterpriseUserId?: number - status: UserActionStatus - skipReason?: UserActionSkipReason - message?: string -} + username: string; + enterpriseUserId?: number; + status: UserActionStatus; + skipReason?: UserActionSkipReason; + message?: string; +}; export type UserActionResult = { - success: boolean - items: UserActionItemResult[] - succeeded: number - skipped: number - failed: number -} + success: boolean; + items: UserActionItemResult[]; + succeeded: number; + skipped: number; + failed: number; +}; export type FormattedUserActionTable = { - headers: string[] - rows: string[][] - summary: string -} + headers: string[]; + rows: string[][]; + summary: string; +}; export enum AliasOperation { - Add = 'add', - Remove = 'remove', + Add = "add", + Remove = "remove", } export type AliasUserInput = { - email: string - operation: AliasOperation - alias: string -} + email: string; + operation: AliasOperation; + alias: string; +}; export type AliasUserResult = { - success: boolean - username: string - enterpriseUserId: number - alias: string - operation: AliasOperation - detail: string -} + success: boolean; + username: string; + enterpriseUserId: number; + alias: string; + operation: AliasOperation; + detail: string; +}; export enum TeamUserStatus { - Added = 'added', - Removed = 'removed', - Skipped = 'skipped', - Failed = 'failed', - MissingPublicKey = 'missing_public_key', + Added = "added", + Removed = "removed", + Skipped = "skipped", + Failed = "failed", + MissingPublicKey = "missing_public_key", } export enum TeamUserSkipReason { - AlreadyMember = 'already_member', - NotMember = 'not_member', + AlreadyMember = "already_member", + NotMember = "not_member", } export type AddUsersToTeamsInput = { - users: string[] - teams: string[] - hideSharedFolders?: boolean -} + users: string[]; + teams: string[]; + hideSharedFolders?: boolean; +}; export type RemoveUsersFromTeamsInput = { - users: string[] - teams: string[] -} + users: string[]; + teams: string[]; +}; export type TeamUserItemResult = { - username: string - enterpriseUserId: number - teamUid: string - teamName: string - status: TeamUserStatus - skipReason?: TeamUserSkipReason - message?: string -} + username: string; + enterpriseUserId: number; + teamUid: string; + teamName: string; + status: TeamUserStatus; + skipReason?: TeamUserSkipReason; + message?: string; +}; export type TeamUserResult = { - success: boolean - items: TeamUserItemResult[] - succeeded: number - skipped: number - failed: number -} + success: boolean; + items: TeamUserItemResult[]; + succeeded: number; + skipped: number; + failed: number; +}; export type FormattedTeamUserTable = { - headers: string[] - rows: string[][] - summary: string -} + headers: string[]; + rows: string[][]; + summary: string; +}; export function normalizeEmailInputs(emails: string[] | undefined): string[] { - return (emails || []).map((e) => e.trim()).filter((e) => e.length > 0) -} - -export function validateUserProfileFields(fullName: string | undefined, jobTitle: string | undefined): void { - if (fullName !== undefined && fullName.length > MAX_FULL_NAME_LENGTH) { - throw new KeeperSdkError( - `Full name exceeds ${MAX_FULL_NAME_LENGTH} characters.`, - ResultCodes.USER_UPDATE_FAILED - ) + return (emails || []).map((e) => e.trim()).filter((e) => e.length > 0); +} + +export function validateUserProfileFields( + fullName: string | undefined, + jobTitle: string | undefined, +): void { + if (fullName !== undefined && fullName.length > MAX_FULL_NAME_LENGTH) { + throw new KeeperSdkError( + `Full name exceeds ${MAX_FULL_NAME_LENGTH} characters.`, + ResultCodes.USER_UPDATE_FAILED, + ); + } + if (jobTitle !== undefined && jobTitle.length > MAX_JOB_TITLE_LENGTH) { + throw new KeeperSdkError( + `Job title exceeds ${MAX_JOB_TITLE_LENGTH} characters.`, + ResultCodes.USER_UPDATE_FAILED, + ); + } +} + +export function resolveExistingUsers( + users: EnterpriseUser[], + identifiers: string[], +): EnterpriseUser[] { + const byEmail = new Map(); + const byId = new Map(); + for (const u of users) { + if (u.username) byEmail.set(u.username.toLowerCase(), u); + byId.set(u.enterprise_user_id, u); + } + + const result: EnterpriseUser[] = []; + const seen = new Set(); + + for (const identifier of identifiers) { + const trimmed = identifier.trim(); + const numericId = Number(trimmed); + let user: EnterpriseUser | undefined; + + if (Number.isInteger(numericId)) { + user = byId.get(numericId); } - if (jobTitle !== undefined && jobTitle.length > MAX_JOB_TITLE_LENGTH) { - throw new KeeperSdkError( - `Job title exceeds ${MAX_JOB_TITLE_LENGTH} characters.`, - ResultCodes.USER_UPDATE_FAILED - ) + if (!user) { + user = byEmail.get(trimmed.toLowerCase()); } -} - -export function resolveExistingUsers(users: EnterpriseUser[], identifiers: string[]): EnterpriseUser[] { - const byEmail = new Map() - const byId = new Map() - for (const u of users) { - if (u.username) byEmail.set(u.username.toLowerCase(), u) - byId.set(u.enterprise_user_id, u) + if (!user) { + throw new KeeperSdkError( + `User "${trimmed}" does not exist.`, + ResultCodes.USER_NOT_FOUND, + ); } - - const result: EnterpriseUser[] = [] - const seen = new Set() - - for (const identifier of identifiers) { - const trimmed = identifier.trim() - const numericId = Number(trimmed) - let user: EnterpriseUser | undefined - - if (Number.isInteger(numericId)) { - user = byId.get(numericId) - } - if (!user) { - user = byEmail.get(trimmed.toLowerCase()) - } - if (!user) { - throw new KeeperSdkError(`User "${trimmed}" does not exist.`, ResultCodes.USER_NOT_FOUND) - } - if (!seen.has(user.enterprise_user_id)) { - seen.add(user.enterprise_user_id) - result.push(user) - } + if (!seen.has(user.enterprise_user_id)) { + seen.add(user.enterprise_user_id); + result.push(user); } + } - return result + return result; } -export type FormattedUserStatus = 'Active' | 'Invited' | 'Locked' | 'Disabled' +export type FormattedUserStatus = "Active" | "Invited" | "Locked" | "Disabled"; export function formatUserStatus(user: EnterpriseUser): FormattedUserStatus { - if (user.status === EnterpriseUserStatus.Invited) return 'Invited' - if (user.lock === 1) return 'Locked' - if ((user.lock ?? 0) > 1) return 'Disabled' - return 'Active' + if (user.status === EnterpriseUserStatus.Invited) return "Invited"; + if (user.lock === 1) return "Locked"; + if ((user.lock ?? 0) > 1) return "Disabled"; + return "Active"; } export function formatTransferStatus(user: EnterpriseUser): string { - const exp = user.account_share_expiration - if (exp != null && exp > 0) { - return new Date(exp) < new Date() ? 'Blocked' : 'Pending Transfer' - } - return '' -} \ No newline at end of file + const exp = user.account_share_expiration; + if (exp != null && exp > 0) { + return new Date(exp) < new Date() ? "Blocked" : "Pending Transfer"; + } + return ""; +} diff --git a/KeeperSdk/src/users/viewUser.ts b/KeeperSdk/src/users/viewUser.ts index d8d4c910..e31abafa 100644 --- a/KeeperSdk/src/users/viewUser.ts +++ b/KeeperSdk/src/users/viewUser.ts @@ -1,284 +1,359 @@ -import { Enterprise, type Auth } from '@keeper-security/keeperapi' -import { extractErrorMessage, isNumber, KeeperSdkError, ResultCodes, logger } from '../utils' +import { Enterprise, type Auth } from "@keeper-security/keeperapi"; import { - EnterpriseDataInclude, - EnterpriseDataManager, - type EnterpriseQueuedTeamRecord, - type EnterpriseQueuedTeamUserLink, - type EnterpriseRoleUserLink, - type EnterpriseTeamRecord, - type EnterpriseTeamUserLink, - type EnterpriseUser, - type EnterpriseUserAliasLink, -} from '../teams/enterpriseData' + extractErrorMessage, + isNumber, + KeeperSdkError, + ResultCodes, + logger, +} from "../utils"; import { - formatTransferStatus, - formatUserStatus, - type FormattedUserViewTable, - type FormatUserViewOptions, - type UserRoleInfo, - type UserTeamInfo, - type UserView, - type UserViewTableRow, -} from './userTypes' - -const NODE_PATH_SEPARATOR = '\\' + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseQueuedTeamRecord, + type EnterpriseQueuedTeamUserLink, + type EnterpriseRoleUserLink, + type EnterpriseTeamRecord, + type EnterpriseTeamUserLink, + type EnterpriseUser, + type EnterpriseUserAliasLink, +} from "../teams/enterpriseData"; +import { + formatTransferStatus, + formatUserStatus, + type FormattedUserViewTable, + type FormatUserViewOptions, + type UserRoleInfo, + type UserTeamInfo, + type UserView, + type UserViewTableRow, +} from "./userTypes"; + +const NODE_PATH_SEPARATOR = "\\"; const VIEW_USER_INCLUDES: EnterpriseDataInclude[] = [ - EnterpriseDataInclude.Users, - EnterpriseDataInclude.Nodes, - EnterpriseDataInclude.Teams, - EnterpriseDataInclude.TeamUsers, - EnterpriseDataInclude.QueuedTeamUsers, - EnterpriseDataInclude.QueuedTeams, - EnterpriseDataInclude.RoleUsers, - EnterpriseDataInclude.Roles, - EnterpriseDataInclude.UserAliases, -] - -export async function viewUser(auth: Auth, identifier: string): Promise { - const enterpriseData = new EnterpriseDataManager(auth) - const [response, displayNames] = await Promise.all([ - enterpriseData.getData(VIEW_USER_INCLUDES), - enterpriseData.getDisplayNames(), - ]) - - const user = resolveUser(response.users || [], identifier) - - const nodes = response.nodes || [] - for (const node of nodes) { - const display = displayNames.nodes.get(node.node_id) - if (display) node.displayName = display - } - const nodeName = EnterpriseDataManager.getNodePath(nodes, user.node_id ?? 0, { - omitRoot: false, - separator: NODE_PATH_SEPARATOR, - }) - - const view: UserView = { - enterprise_user_id: user.enterprise_user_id, - username: user.username, - node_id: user.node_id ?? 0, - node_name: nodeName, - full_name: (user.full_name || '').trim(), - status: formatUserStatus(user), - tfa_enabled: user.tfa_enabled ?? false, - transfer_status: formatTransferStatus(user), - } - - const aliases = buildAliases(response.user_aliases || [], user) - if (aliases.length > 0) view.aliases = aliases - - const teams = buildUserTeams(response.team_users || [], response.teams || [], user.enterprise_user_id) - if (teams.length > 0) view.teams = teams - - const queuedTeams = buildUserQueuedTeams( - response.queued_team_users || [], - response.teams || [], - response.queued_teams || [], - user.enterprise_user_id - ) - if (queuedTeams.length > 0) view.queued_teams = queuedTeams - - const roles = buildUserRoles(response.role_users || [], displayNames.roles, user.enterprise_user_id) - if (roles.length > 0) view.roles = roles - - const shareAdmins = await fetchShareAdmins(auth, user.username) - if (shareAdmins.length > 0) view.share_admins = shareAdmins - - return view + EnterpriseDataInclude.Users, + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.TeamUsers, + EnterpriseDataInclude.QueuedTeamUsers, + EnterpriseDataInclude.QueuedTeams, + EnterpriseDataInclude.RoleUsers, + EnterpriseDataInclude.Roles, + EnterpriseDataInclude.UserAliases, +]; + +export async function viewUser( + auth: Auth, + identifier: string, +): Promise { + const enterpriseData = new EnterpriseDataManager(auth); + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(VIEW_USER_INCLUDES), + enterpriseData.getDisplayNames(), + ]); + + const user = resolveUser(response.users || [], identifier); + + const nodes = response.nodes || []; + for (const node of nodes) { + const display = displayNames.nodes.get(node.node_id); + if (display) node.displayName = display; + } + const nodeName = EnterpriseDataManager.getNodePath(nodes, user.node_id ?? 0, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }); + + const view: UserView = { + enterprise_user_id: user.enterprise_user_id, + username: user.username, + node_id: user.node_id ?? 0, + node_name: nodeName, + full_name: (user.full_name || "").trim(), + status: formatUserStatus(user), + tfa_enabled: user.tfa_enabled ?? false, + transfer_status: formatTransferStatus(user), + }; + + const aliases = buildAliases(response.user_aliases || [], user); + if (aliases.length > 0) view.aliases = aliases; + + const teams = buildUserTeams( + response.team_users || [], + response.teams || [], + user.enterprise_user_id, + ); + if (teams.length > 0) view.teams = teams; + + const queuedTeams = buildUserQueuedTeams( + response.queued_team_users || [], + response.teams || [], + response.queued_teams || [], + user.enterprise_user_id, + ); + if (queuedTeams.length > 0) view.queued_teams = queuedTeams; + + const roles = buildUserRoles( + response.role_users || [], + displayNames.roles, + user.enterprise_user_id, + ); + if (roles.length > 0) view.roles = roles; + + const shareAdmins = await fetchShareAdmins(auth, user.username); + if (shareAdmins.length > 0) view.share_admins = shareAdmins; + + return view; } -export function formatUserView(view: UserView, options: FormatUserViewOptions = {}): FormattedUserViewTable { - const verbose = options.verbose === true - - const rows: UserViewTableRow[] = [ - { label: 'User ID', value: String(view.enterprise_user_id) }, - { label: 'Email', value: view.username }, - { label: 'Full Name', value: view.full_name }, - { label: 'Node Name', value: view.node_name, id: verbose ? String(view.node_id) : undefined }, - { label: 'Status', value: view.status }, - { label: 'Transfer Status', value: view.transfer_status }, - { label: '2FA Enabled', value: String(view.tfa_enabled) }, - ] - - if (view.aliases && view.aliases.length > 0) { - rows.push({ label: 'Email Alias(es)', value: view.aliases }) - } - if (view.teams && view.teams.length > 0) { - rows.push({ - label: 'Team(s)', - value: view.teams.map((t) => t.name), - id: verbose ? view.teams.map((t) => t.team_uid) : undefined, - }) - } - if (view.queued_teams && view.queued_teams.length > 0) { - rows.push({ - label: 'Queued Team(s)', - value: view.queued_teams.map((t) => t.name), - id: verbose ? view.queued_teams.map((t) => t.team_uid) : undefined, - }) - } - if (view.roles && view.roles.length > 0) { - rows.push({ - label: 'Role(s)', - value: view.roles.map((r) => r.role_name), - id: verbose ? view.roles.map((r) => String(r.role_id)) : undefined, - }) - } - if (view.share_admins && view.share_admins.length > 0) { - rows.push({ label: 'Share Admin(s)', value: view.share_admins }) - } - - return { rows, hasIdColumn: verbose } +export function formatUserView( + view: UserView, + options: FormatUserViewOptions = {}, +): FormattedUserViewTable { + const verbose = options.verbose === true; + + const rows: UserViewTableRow[] = [ + { label: "User ID", value: String(view.enterprise_user_id) }, + { label: "Email", value: view.username }, + { label: "Full Name", value: view.full_name }, + { + label: "Node Name", + value: view.node_name, + id: verbose ? String(view.node_id) : undefined, + }, + { label: "Status", value: view.status }, + { label: "Transfer Status", value: view.transfer_status }, + { label: "2FA Enabled", value: String(view.tfa_enabled) }, + ]; + + if (view.aliases && view.aliases.length > 0) { + rows.push({ label: "Email Alias(es)", value: view.aliases }); + } + if (view.teams && view.teams.length > 0) { + rows.push({ + label: "Team(s)", + value: view.teams.map((t) => t.name), + id: verbose ? view.teams.map((t) => t.team_uid) : undefined, + }); + } + if (view.queued_teams && view.queued_teams.length > 0) { + rows.push({ + label: "Queued Team(s)", + value: view.queued_teams.map((t) => t.name), + id: verbose ? view.queued_teams.map((t) => t.team_uid) : undefined, + }); + } + if (view.roles && view.roles.length > 0) { + rows.push({ + label: "Role(s)", + value: view.roles.map((r) => r.role_name), + id: verbose ? view.roles.map((r) => String(r.role_id)) : undefined, + }); + } + if (view.share_admins && view.share_admins.length > 0) { + rows.push({ label: "Share Admin(s)", value: view.share_admins }); + } + + return { rows, hasIdColumn: verbose }; } export function userViewTable(table: FormattedUserViewTable): string { - const labelWidth = table.rows.reduce((max, row) => Math.max(max, row.label.length), 0) - - const expandedRows = table.rows.map((row) => ({ - label: row.label, - valueLines: asLines(row.value), - idLines: table.hasIdColumn ? asLines(row.id ?? []) : [], - })) - - const valueWidth = expandedRows.reduce( - (max, row) => Math.max(max, ...row.valueLines.map((l) => l.length)), - 0 - ) - const idWidth = table.hasIdColumn - ? expandedRows.reduce((max, row) => Math.max(max, ...row.idLines.map((l) => l.length)), 0) - : 0 - - const padLeft = (text: string, width: number): string => - ' '.repeat(Math.max(0, width - text.length)) + text - const padRight = (text: string, width: number): string => - text + ' '.repeat(Math.max(0, width - text.length)) - - const lines: string[] = [] - for (const row of expandedRows) { - const lineCount = Math.max(row.valueLines.length, table.hasIdColumn ? row.idLines.length : 0, 1) - for (let li = 0; li < lineCount; li += 1) { - const labelCell = padLeft(li === 0 ? row.label : '', labelWidth) - const valueCell = padRight(row.valueLines[li] ?? '', valueWidth) - const cells: string[] = [labelCell, valueCell] - if (table.hasIdColumn) cells.push(padRight(row.idLines[li] ?? '', idWidth)) - lines.push(cells.join(' ').trimEnd()) - } + const labelWidth = table.rows.reduce( + (max, row) => Math.max(max, row.label.length), + 0, + ); + + const expandedRows = table.rows.map((row) => ({ + label: row.label, + valueLines: asLines(row.value), + idLines: table.hasIdColumn ? asLines(row.id ?? []) : [], + })); + + const valueWidth = expandedRows.reduce( + (max, row) => Math.max(max, ...row.valueLines.map((l) => l.length)), + 0, + ); + const idWidth = table.hasIdColumn + ? expandedRows.reduce( + (max, row) => Math.max(max, ...row.idLines.map((l) => l.length)), + 0, + ) + : 0; + + const padLeft = (text: string, width: number): string => + " ".repeat(Math.max(0, width - text.length)) + text; + const padRight = (text: string, width: number): string => + text + " ".repeat(Math.max(0, width - text.length)); + + const lines: string[] = []; + for (const row of expandedRows) { + const lineCount = Math.max( + row.valueLines.length, + table.hasIdColumn ? row.idLines.length : 0, + 1, + ); + for (let li = 0; li < lineCount; li += 1) { + const labelCell = padLeft(li === 0 ? row.label : "", labelWidth); + const valueCell = padRight(row.valueLines[li] ?? "", valueWidth); + const cells: string[] = [labelCell, valueCell]; + if (table.hasIdColumn) + cells.push(padRight(row.idLines[li] ?? "", idWidth)); + lines.push(cells.join(" ").trimEnd()); } - return lines.join('\n') + } + return lines.join("\n"); } -function resolveUser(users: EnterpriseUser[], identifier: string): EnterpriseUser { - const trimmed = (identifier ?? '').trim() - if (!trimmed) { - throw new KeeperSdkError('User email or ID is required.', ResultCodes.USER_NOT_FOUND) - } - - const numericId = Number(trimmed) - if (Number.isInteger(numericId)) { - const byId = users.find((u) => u.enterprise_user_id === numericId) - if (byId) return byId - } - - const lowered = trimmed.toLowerCase() - const byEmail = users.filter((u) => (u.username || '').toLowerCase() === lowered) - if (byEmail.length === 1) return byEmail[0] - if (byEmail.length > 1) { - throw new KeeperSdkError( - `Multiple users match "${trimmed}". Specify the enterprise user ID instead.`, - ResultCodes.MULTIPLE_USER_MATCHES - ) - } - - throw new KeeperSdkError(`User "${trimmed}" does not exist.`, ResultCodes.USER_NOT_FOUND) +function resolveUser( + users: EnterpriseUser[], + identifier: string, +): EnterpriseUser { + const trimmed = (identifier ?? "").trim(); + if (!trimmed) { + throw new KeeperSdkError( + "User email or ID is required.", + ResultCodes.USER_NOT_FOUND, + ); + } + + const numericId = Number(trimmed); + if (Number.isInteger(numericId)) { + const byId = users.find((u) => u.enterprise_user_id === numericId); + if (byId) return byId; + } + + const lowered = trimmed.toLowerCase(); + const byEmail = users.filter( + (u) => (u.username || "").toLowerCase() === lowered, + ); + if (byEmail.length === 1) return byEmail[0]; + if (byEmail.length > 1) { + throw new KeeperSdkError( + `Multiple users match "${trimmed}". Specify the enterprise user ID instead.`, + ResultCodes.MULTIPLE_USER_MATCHES, + ); + } + + throw new KeeperSdkError( + `User "${trimmed}" does not exist.`, + ResultCodes.USER_NOT_FOUND, + ); } -function buildAliases(links: EnterpriseUserAliasLink[], user: EnterpriseUser): string[] { - const primary = user.username.toLowerCase() - return links - .filter((l) => l.enterprise_user_id === user.enterprise_user_id && l.username.toLowerCase() !== primary) - .map((l) => l.username) - .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) +function buildAliases( + links: EnterpriseUserAliasLink[], + user: EnterpriseUser, +): string[] { + const primary = user.username.toLowerCase(); + return links + .filter( + (l) => + l.enterprise_user_id === user.enterprise_user_id && + l.username.toLowerCase() !== primary, + ) + .map((l) => l.username) + .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); } function buildUserTeams( - teamUsers: EnterpriseTeamUserLink[], - teams: EnterpriseTeamRecord[], - userId: number + teamUsers: EnterpriseTeamUserLink[], + teams: EnterpriseTeamRecord[], + userId: number, ): UserTeamInfo[] { - const teamByUid = new Map(teams.filter((t) => t.team_uid).map((t) => [t.team_uid, t])) - - return teamUsers - .filter((link) => link.enterprise_user_id === userId) - .flatMap((link) => { - const team = teamByUid.get(link.team_uid) - return team ? [{ team_uid: team.team_uid, name: team.name || team.team_uid }] : [] - }) - .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) + const teamByUid = new Map( + teams.filter((t) => t.team_uid).map((t) => [t.team_uid, t]), + ); + + return teamUsers + .filter((link) => link.enterprise_user_id === userId) + .flatMap((link) => { + const team = teamByUid.get(link.team_uid); + return team + ? [{ team_uid: team.team_uid, name: team.name || team.team_uid }] + : []; + }) + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); } function buildUserQueuedTeams( - queuedTeamUsers: EnterpriseQueuedTeamUserLink[], - teams: EnterpriseTeamRecord[], - queuedTeams: EnterpriseQueuedTeamRecord[], - userId: number + queuedTeamUsers: EnterpriseQueuedTeamUserLink[], + teams: EnterpriseTeamRecord[], + queuedTeams: EnterpriseQueuedTeamRecord[], + userId: number, ): UserTeamInfo[] { - const teamByUid = new Map() - for (const t of teams) { - if (t.team_uid) teamByUid.set(t.team_uid, t.name || t.team_uid) - } - for (const qt of queuedTeams) { - if (qt.team_uid && !teamByUid.has(qt.team_uid)) teamByUid.set(qt.team_uid, qt.name || qt.team_uid) - } - - return queuedTeamUsers - .filter((link) => link.enterprise_user_id === userId) - .flatMap((link) => { - const name = teamByUid.get(link.team_uid) - return name ? [{ team_uid: link.team_uid, name }] : [] - }) - .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) + const teamByUid = new Map(); + for (const t of teams) { + if (t.team_uid) teamByUid.set(t.team_uid, t.name || t.team_uid); + } + for (const qt of queuedTeams) { + if (qt.team_uid && !teamByUid.has(qt.team_uid)) + teamByUid.set(qt.team_uid, qt.name || qt.team_uid); + } + + return queuedTeamUsers + .filter((link) => link.enterprise_user_id === userId) + .flatMap((link) => { + const name = teamByUid.get(link.team_uid); + return name ? [{ team_uid: link.team_uid, name }] : []; + }) + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); } function buildUserRoles( - roleUsers: EnterpriseRoleUserLink[], - decryptedRoleNames: Map, - userId: number + roleUsers: EnterpriseRoleUserLink[], + decryptedRoleNames: Map, + userId: number, ): UserRoleInfo[] { - return roleUsers - .filter((link) => link.enterprise_user_id === userId && isNumber(link.role_id)) - .map((link) => ({ - role_id: link.role_id, - role_name: decryptedRoleNames.get(link.role_id) || String(link.role_id), - })) - .sort((a, b) => a.role_name.localeCompare(b.role_name, undefined, { sensitivity: 'base' })) + return roleUsers + .filter( + (link) => link.enterprise_user_id === userId && isNumber(link.role_id), + ) + .map((link) => ({ + role_id: link.role_id, + role_name: decryptedRoleNames.get(link.role_id) || String(link.role_id), + })) + .sort((a, b) => + a.role_name.localeCompare(b.role_name, undefined, { + sensitivity: "base", + }), + ); } -async function fetchShareAdmins(auth: Auth, username: string): Promise { - try { - const message = { - path: 'enterprise/get_sharing_admins', - toBytes(): Uint8Array { - return Enterprise.GetSharingAdminsRequest.encode({ username }).finish() - }, - fromBytes(data: Uint8Array): Enterprise.IGetSharingAdminsResponse { - return Enterprise.GetSharingAdminsResponse.decode(data) - }, - } - const response = await auth.executeRest(message) - return (response.userProfileExts || []) - .map((ext) => ext.email || '') - .filter((email) => email.length > 0) - .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) - } catch (err) { - const masked = username.includes('@') ? `***@${username.split('@')[1]}` : '***' - logger.warn(`get_sharing_admins failed for ${masked}: ${extractErrorMessage(err)}`) - return [] - } +async function fetchShareAdmins( + auth: Auth, + username: string, +): Promise { + try { + const message = { + path: "enterprise/get_sharing_admins", + toBytes(): Uint8Array { + return Enterprise.GetSharingAdminsRequest.encode({ username }).finish(); + }, + fromBytes(data: Uint8Array): Enterprise.IGetSharingAdminsResponse { + return Enterprise.GetSharingAdminsResponse.decode(data); + }, + }; + const response = await auth.executeRest(message); + return (response.userProfileExts || []) + .map((ext) => ext.email || "") + .filter((email) => email.length > 0) + .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); + } catch (err) { + const masked = username.includes("@") + ? `***@${username.split("@")[1]}` + : "***"; + logger.warn( + `get_sharing_admins failed for ${masked}: ${extractErrorMessage(err)}`, + ); + return []; + } } function asLines(value: string | string[]): string[] { - if (Array.isArray(value)) return value.length > 0 ? value : [''] - return value.length > 0 ? [value] : [''] + if (Array.isArray(value)) return value.length > 0 ? value : [""]; + return value.length > 0 ? [value] : [""]; } diff --git a/KeeperSdk/src/utils/Logger.ts b/KeeperSdk/src/utils/Logger.ts index 40847e5f..b362575d 100644 --- a/KeeperSdk/src/utils/Logger.ts +++ b/KeeperSdk/src/utils/Logger.ts @@ -1,78 +1,82 @@ export enum LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - NONE = 4, + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + NONE = 4, } export interface ILogger { - debug(...args: unknown[]): void - info(...args: unknown[]): void - warn(...args: unknown[]): void - error(...args: unknown[]): void + debug(...args: unknown[]): void; + info(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; } -const CREDENTIAL_PATTERN = /\b(password|passwd|pwd|secret|api[_-]?key|auth[_-]?token)\s*[:=]\s*\S+/gi +const CREDENTIAL_PATTERN = + /\b(password|passwd|pwd|secret|api[_-]?key|auth[_-]?token)\s*[:=]\s*\S+/gi; function sanitizeArg(arg: unknown): unknown { - if (typeof arg !== 'string') return arg - return arg.replace(CREDENTIAL_PATTERN, (_, key: string) => `${key}=[REDACTED]`) + if (typeof arg !== "string") return arg; + return arg.replace( + CREDENTIAL_PATTERN, + (_, key: string) => `${key}=[REDACTED]`, + ); } export class ConsoleLogger implements ILogger { - private level: LogLevel + private level: LogLevel; - constructor(level: LogLevel = LogLevel.INFO) { - this.level = level - } + constructor(level: LogLevel = LogLevel.INFO) { + this.level = level; + } - public setLevel(level: LogLevel): void { - this.level = level - } + public setLevel(level: LogLevel): void { + this.level = level; + } - public getLevel(): LogLevel { - return this.level - } + public getLevel(): LogLevel { + return this.level; + } - public debug(...args: unknown[]): void { - if (this.level <= LogLevel.DEBUG) console.debug(...args.map(sanitizeArg)) - } + public debug(...args: unknown[]): void { + if (this.level <= LogLevel.DEBUG) console.debug(...args.map(sanitizeArg)); + } - public info(...args: unknown[]): void { - if (this.level <= LogLevel.INFO) console.log(...args.map(sanitizeArg)) - } + public info(...args: unknown[]): void { + if (this.level <= LogLevel.INFO) console.log(...args.map(sanitizeArg)); + } - public warn(...args: unknown[]): void { - if (this.level <= LogLevel.WARN) console.warn(...args.map(sanitizeArg)) - } + public warn(...args: unknown[]): void { + if (this.level <= LogLevel.WARN) console.warn(...args.map(sanitizeArg)); + } - public error(...args: unknown[]): void { - if (this.level <= LogLevel.ERROR) console.error(...args.map(sanitizeArg)) - } + public error(...args: unknown[]): void { + if (this.level <= LogLevel.ERROR) console.error(...args.map(sanitizeArg)); + } } -let globalLogger: ILogger = new ConsoleLogger() +let globalLogger: ILogger = new ConsoleLogger(); export function setLogger(newLogger: ILogger): void { - globalLogger = newLogger + globalLogger = newLogger; } export function getLogger(): ILogger { - return globalLogger + return globalLogger; } export function resetLogger(level: LogLevel = LogLevel.INFO): ConsoleLogger { - const c = new ConsoleLogger(level) - globalLogger = c - return c + const c = new ConsoleLogger(level); + globalLogger = c; + return c; } export const logger: ILogger = { - debug: (...args) => globalLogger.debug(...args), - info: (...args) => globalLogger.info(...args), - warn: (...args) => globalLogger.warn(...args), - error: (...args) => globalLogger.error(...args), -} + debug: (...args) => globalLogger.debug(...args), + info: (...args) => globalLogger.info(...args), + warn: (...args) => globalLogger.warn(...args), + error: (...args) => globalLogger.error(...args), +}; -export { ConsoleLogger as Logger } +export { ConsoleLogger as Logger }; diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index a50889ce..d14741af 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -1,166 +1,166 @@ export const SdkDefaults = { - CLIENT_VERSION: 'c17.0.0', - DEVICE_NAME: 'JavaScript Keeper SDK', - CONFIG_DIR: '.keeper', - LOG_FORMAT: '!', -} as const + CLIENT_VERSION: "c17.0.0", + DEVICE_NAME: "JavaScript Keeper SDK", + CONFIG_DIR: ".keeper", + LOG_FORMAT: "!", +} as const; export const AuthDefaults = { - MAX_LOGIN_ATTEMPTS: 5, - APPROVAL_TIMEOUT_MS: 60_000, - CODE_VALIDATION_DELAY_MS: 2_000, -} as const + MAX_LOGIN_ATTEMPTS: 5, + APPROVAL_TIMEOUT_MS: 60_000, + CODE_VALIDATION_DELAY_MS: 2_000, +} as const; export enum AuthErrorCode { - InvalidCredentials = 'invalid_credentials', - MissingUsername = 'missing_username', - MissingPassword = 'missing_password', - MaxAttemptsExceeded = 'max_attempts_exceeded', - UserCancelled = 'user_cancelled', - Unsupported2FAChannel = 'unsupported_2fa_channel', + InvalidCredentials = "invalid_credentials", + MissingUsername = "missing_username", + MissingPassword = "missing_password", + MaxAttemptsExceeded = "max_attempts_exceeded", + UserCancelled = "user_cancelled", + Unsupported2FAChannel = "unsupported_2fa_channel", } export enum SessionErrorCode { - NotLoggedIn = 'not_logged_in', - DeviceNotRegistered = 'device_not_registered', - NoPreviousLogin = 'no_previous_login', - NoCloneCode = 'no_clone_code', - PersistentLoginFailed = 'persistent_login_failed', - SessionTokenExpired = 'session_token_expired', + NotLoggedIn = "not_logged_in", + DeviceNotRegistered = "device_not_registered", + NoPreviousLogin = "no_previous_login", + NoCloneCode = "no_clone_code", + PersistentLoginFailed = "persistent_login_failed", + SessionTokenExpired = "session_token_expired", } export enum ValidationErrorCode { - InvalidPattern = 'invalid_pattern', + InvalidPattern = "invalid_pattern", } export enum RoleErrorCode { - RoleRequired = 'role_required', - RoleNotFound = 'role_not_found', - MultipleRoleMatches = 'multiple_role_matches', - NoRolesToAdd = 'no_roles_to_add', - NoRolesToUpdate = 'no_roles_to_update', - NoRolesToDelete = 'no_roles_to_delete', - RoleAddFailed = 'role_add_failed', - RoleUpdateFailed = 'role_update_failed', - RoleDeleteFailed = 'role_delete_failed', - RoleIdAllocationFailed = 'role_id_allocation_failed', - RoleRenameMultiNotAllowed = 'role_rename_multi_not_allowed', - RoleNameEmpty = 'role_name_empty', - RoleEnforcementFailed = 'role_enforcement_failed', + RoleRequired = "role_required", + RoleNotFound = "role_not_found", + MultipleRoleMatches = "multiple_role_matches", + NoRolesToAdd = "no_roles_to_add", + NoRolesToUpdate = "no_roles_to_update", + NoRolesToDelete = "no_roles_to_delete", + RoleAddFailed = "role_add_failed", + RoleUpdateFailed = "role_update_failed", + RoleDeleteFailed = "role_delete_failed", + RoleIdAllocationFailed = "role_id_allocation_failed", + RoleRenameMultiNotAllowed = "role_rename_multi_not_allowed", + RoleNameEmpty = "role_name_empty", + RoleEnforcementFailed = "role_enforcement_failed", } export enum TeamErrorCode { - TeamRequired = 'team_required', - TeamNotFound = 'team_not_found', - MultipleTeamMatches = 'multiple_team_matches', - NoTeamsToAdd = 'no_teams_to_add', - NoTeamsToUpdate = 'no_teams_to_update', - NoTeamsToDelete = 'no_teams_to_delete', - ParentNodeRequired = 'parent_node_required', - ParentNodeNotFound = 'parent_node_not_found', - MultipleParentNodeMatches = 'multiple_parent_node_matches', - EnterprisePublicKeyMissing = 'enterprise_public_key_missing', - EnterpriseTreeKeyUnavailable = 'enterprise_tree_key_unavailable', - DataKeyMissing = 'data_key_missing', - TeamAddFailed = 'team_add_failed', - TeamUpdateFailed = 'team_update_failed', - TeamDeleteFailed = 'team_delete_failed', - MultipleTeamRenameNotAllowed = 'multiple_team_rename_not_allowed', - QueuedTeamNotFound = 'queued_team_not_found', - TeamNameTooLong = 'team_name_too_long', + TeamRequired = "team_required", + TeamNotFound = "team_not_found", + MultipleTeamMatches = "multiple_team_matches", + NoTeamsToAdd = "no_teams_to_add", + NoTeamsToUpdate = "no_teams_to_update", + NoTeamsToDelete = "no_teams_to_delete", + ParentNodeRequired = "parent_node_required", + ParentNodeNotFound = "parent_node_not_found", + MultipleParentNodeMatches = "multiple_parent_node_matches", + EnterprisePublicKeyMissing = "enterprise_public_key_missing", + EnterpriseTreeKeyUnavailable = "enterprise_tree_key_unavailable", + DataKeyMissing = "data_key_missing", + TeamAddFailed = "team_add_failed", + TeamUpdateFailed = "team_update_failed", + TeamDeleteFailed = "team_delete_failed", + MultipleTeamRenameNotAllowed = "multiple_team_rename_not_allowed", + QueuedTeamNotFound = "queued_team_not_found", + TeamNameTooLong = "team_name_too_long", } export enum UserErrorCode { - NoUsersToUpdate = 'no_users_to_update', - NoUsersToAdd = 'no_users_to_add', - NoUsersToDelete = 'no_users_to_delete', - UserNotFound = 'user_not_found', - MultipleUserMatches = 'multiple_user_matches', - UserAddFailed = 'user_add_failed', - UserUpdateFailed = 'user_update_failed', - UserDeleteFailed = 'user_delete_failed', - RoleNotFound = 'role_not_found', - NoUsersToAction = 'no_users_to_action', - UserActionFailed = 'user_action_failed', - UserActionAllNotSupported = 'user_action_all_not_supported', - UserAliasFailed = 'user_alias_failed', - NoTeamsForUserOp = 'no_teams_for_user_op', - TeamUserAddFailed = 'team_user_add_failed', - TeamUserRemoveFailed = 'team_user_remove_failed', + NoUsersToUpdate = "no_users_to_update", + NoUsersToAdd = "no_users_to_add", + NoUsersToDelete = "no_users_to_delete", + UserNotFound = "user_not_found", + MultipleUserMatches = "multiple_user_matches", + UserAddFailed = "user_add_failed", + UserUpdateFailed = "user_update_failed", + UserDeleteFailed = "user_delete_failed", + RoleNotFound = "role_not_found", + NoUsersToAction = "no_users_to_action", + UserActionFailed = "user_action_failed", + UserActionAllNotSupported = "user_action_all_not_supported", + UserAliasFailed = "user_alias_failed", + NoTeamsForUserOp = "no_teams_for_user_op", + TeamUserAddFailed = "team_user_add_failed", + TeamUserRemoveFailed = "team_user_remove_failed", } export enum SyncErrorCode { - SyncFailed = 'sync_failed', + SyncFailed = "sync_failed", } export const ResultCodes = { - INVALID_CREDENTIALS: AuthErrorCode.InvalidCredentials, - MISSING_USERNAME: AuthErrorCode.MissingUsername, - MISSING_PASSWORD: AuthErrorCode.MissingPassword, - MAX_ATTEMPTS_EXCEEDED: AuthErrorCode.MaxAttemptsExceeded, - USER_CANCELLED: AuthErrorCode.UserCancelled, - UNSUPPORTED_2FA_CHANNEL: AuthErrorCode.Unsupported2FAChannel, - NOT_LOGGED_IN: SessionErrorCode.NotLoggedIn, - DEVICE_NOT_REGISTERED: SessionErrorCode.DeviceNotRegistered, - NO_PREVIOUS_LOGIN: SessionErrorCode.NoPreviousLogin, - NO_CLONE_CODE: SessionErrorCode.NoCloneCode, - PERSISTENT_LOGIN_FAILED: SessionErrorCode.PersistentLoginFailed, - SESSION_TOKEN_EXPIRED: SessionErrorCode.SessionTokenExpired, - INVALID_PATTERN: ValidationErrorCode.InvalidPattern, - ROLE_REQUIRED: RoleErrorCode.RoleRequired, - ROLE_NOT_FOUND: RoleErrorCode.RoleNotFound, - MULTIPLE_ROLE_MATCHES: RoleErrorCode.MultipleRoleMatches, - NO_ROLES_TO_ADD: RoleErrorCode.NoRolesToAdd, - NO_ROLES_TO_UPDATE: RoleErrorCode.NoRolesToUpdate, - NO_ROLES_TO_DELETE: RoleErrorCode.NoRolesToDelete, - ROLE_ADD_FAILED: RoleErrorCode.RoleAddFailed, - ROLE_UPDATE_FAILED: RoleErrorCode.RoleUpdateFailed, - ROLE_DELETE_FAILED: RoleErrorCode.RoleDeleteFailed, - ROLE_ID_ALLOCATION_FAILED: RoleErrorCode.RoleIdAllocationFailed, - ROLE_RENAME_MULTI_NOT_ALLOWED: RoleErrorCode.RoleRenameMultiNotAllowed, - ROLE_NAME_EMPTY: RoleErrorCode.RoleNameEmpty, - ROLE_ENFORCEMENT_FAILED: RoleErrorCode.RoleEnforcementFailed, - TEAM_REQUIRED: TeamErrorCode.TeamRequired, - TEAM_NOT_FOUND: TeamErrorCode.TeamNotFound, - MULTIPLE_TEAM_MATCHES: TeamErrorCode.MultipleTeamMatches, - NO_TEAMS_TO_ADD: TeamErrorCode.NoTeamsToAdd, - NO_TEAMS_TO_UPDATE: TeamErrorCode.NoTeamsToUpdate, - NO_TEAMS_TO_DELETE: TeamErrorCode.NoTeamsToDelete, - PARENT_NODE_REQUIRED: TeamErrorCode.ParentNodeRequired, - PARENT_NODE_NOT_FOUND: TeamErrorCode.ParentNodeNotFound, - MULTIPLE_PARENT_NODE_MATCHES: TeamErrorCode.MultipleParentNodeMatches, - ENTERPRISE_PUBLIC_KEY_MISSING: TeamErrorCode.EnterprisePublicKeyMissing, - ENTERPRISE_TREE_KEY_UNAVAILABLE: TeamErrorCode.EnterpriseTreeKeyUnavailable, - DATA_KEY_MISSING: TeamErrorCode.DataKeyMissing, - TEAM_ADD_FAILED: TeamErrorCode.TeamAddFailed, - TEAM_UPDATE_FAILED: TeamErrorCode.TeamUpdateFailed, - TEAM_DELETE_FAILED: TeamErrorCode.TeamDeleteFailed, - MULTIPLE_TEAM_RENAME_NOT_ALLOWED: TeamErrorCode.MultipleTeamRenameNotAllowed, - QUEUED_TEAM_NOT_FOUND: TeamErrorCode.QueuedTeamNotFound, - TEAM_NAME_TOO_LONG: TeamErrorCode.TeamNameTooLong, - NO_USERS_TO_UPDATE: UserErrorCode.NoUsersToUpdate, - NO_USERS_TO_ADD: UserErrorCode.NoUsersToAdd, - NO_USERS_TO_DELETE: UserErrorCode.NoUsersToDelete, - USER_NOT_FOUND: UserErrorCode.UserNotFound, - MULTIPLE_USER_MATCHES: UserErrorCode.MultipleUserMatches, - USER_ADD_FAILED: UserErrorCode.UserAddFailed, - USER_UPDATE_FAILED: UserErrorCode.UserUpdateFailed, - USER_DELETE_FAILED: UserErrorCode.UserDeleteFailed, - NO_USERS_TO_ACTION: UserErrorCode.NoUsersToAction, - USER_ACTION_FAILED: UserErrorCode.UserActionFailed, - USER_ACTION_ALL_NOT_SUPPORTED: UserErrorCode.UserActionAllNotSupported, - USER_ALIAS_FAILED: UserErrorCode.UserAliasFailed, - NO_TEAMS_FOR_USER_OP: UserErrorCode.NoTeamsForUserOp, - TEAM_USER_ADD_FAILED: UserErrorCode.TeamUserAddFailed, - TEAM_USER_REMOVE_FAILED: UserErrorCode.TeamUserRemoveFailed, - SYNC_FAILED: SyncErrorCode.SyncFailed, -} as const + INVALID_CREDENTIALS: AuthErrorCode.InvalidCredentials, + MISSING_USERNAME: AuthErrorCode.MissingUsername, + MISSING_PASSWORD: AuthErrorCode.MissingPassword, + MAX_ATTEMPTS_EXCEEDED: AuthErrorCode.MaxAttemptsExceeded, + USER_CANCELLED: AuthErrorCode.UserCancelled, + UNSUPPORTED_2FA_CHANNEL: AuthErrorCode.Unsupported2FAChannel, + NOT_LOGGED_IN: SessionErrorCode.NotLoggedIn, + DEVICE_NOT_REGISTERED: SessionErrorCode.DeviceNotRegistered, + NO_PREVIOUS_LOGIN: SessionErrorCode.NoPreviousLogin, + NO_CLONE_CODE: SessionErrorCode.NoCloneCode, + PERSISTENT_LOGIN_FAILED: SessionErrorCode.PersistentLoginFailed, + SESSION_TOKEN_EXPIRED: SessionErrorCode.SessionTokenExpired, + INVALID_PATTERN: ValidationErrorCode.InvalidPattern, + ROLE_REQUIRED: RoleErrorCode.RoleRequired, + ROLE_NOT_FOUND: RoleErrorCode.RoleNotFound, + MULTIPLE_ROLE_MATCHES: RoleErrorCode.MultipleRoleMatches, + NO_ROLES_TO_ADD: RoleErrorCode.NoRolesToAdd, + NO_ROLES_TO_UPDATE: RoleErrorCode.NoRolesToUpdate, + NO_ROLES_TO_DELETE: RoleErrorCode.NoRolesToDelete, + ROLE_ADD_FAILED: RoleErrorCode.RoleAddFailed, + ROLE_UPDATE_FAILED: RoleErrorCode.RoleUpdateFailed, + ROLE_DELETE_FAILED: RoleErrorCode.RoleDeleteFailed, + ROLE_ID_ALLOCATION_FAILED: RoleErrorCode.RoleIdAllocationFailed, + ROLE_RENAME_MULTI_NOT_ALLOWED: RoleErrorCode.RoleRenameMultiNotAllowed, + ROLE_NAME_EMPTY: RoleErrorCode.RoleNameEmpty, + ROLE_ENFORCEMENT_FAILED: RoleErrorCode.RoleEnforcementFailed, + TEAM_REQUIRED: TeamErrorCode.TeamRequired, + TEAM_NOT_FOUND: TeamErrorCode.TeamNotFound, + MULTIPLE_TEAM_MATCHES: TeamErrorCode.MultipleTeamMatches, + NO_TEAMS_TO_ADD: TeamErrorCode.NoTeamsToAdd, + NO_TEAMS_TO_UPDATE: TeamErrorCode.NoTeamsToUpdate, + NO_TEAMS_TO_DELETE: TeamErrorCode.NoTeamsToDelete, + PARENT_NODE_REQUIRED: TeamErrorCode.ParentNodeRequired, + PARENT_NODE_NOT_FOUND: TeamErrorCode.ParentNodeNotFound, + MULTIPLE_PARENT_NODE_MATCHES: TeamErrorCode.MultipleParentNodeMatches, + ENTERPRISE_PUBLIC_KEY_MISSING: TeamErrorCode.EnterprisePublicKeyMissing, + ENTERPRISE_TREE_KEY_UNAVAILABLE: TeamErrorCode.EnterpriseTreeKeyUnavailable, + DATA_KEY_MISSING: TeamErrorCode.DataKeyMissing, + TEAM_ADD_FAILED: TeamErrorCode.TeamAddFailed, + TEAM_UPDATE_FAILED: TeamErrorCode.TeamUpdateFailed, + TEAM_DELETE_FAILED: TeamErrorCode.TeamDeleteFailed, + MULTIPLE_TEAM_RENAME_NOT_ALLOWED: TeamErrorCode.MultipleTeamRenameNotAllowed, + QUEUED_TEAM_NOT_FOUND: TeamErrorCode.QueuedTeamNotFound, + TEAM_NAME_TOO_LONG: TeamErrorCode.TeamNameTooLong, + NO_USERS_TO_UPDATE: UserErrorCode.NoUsersToUpdate, + NO_USERS_TO_ADD: UserErrorCode.NoUsersToAdd, + NO_USERS_TO_DELETE: UserErrorCode.NoUsersToDelete, + USER_NOT_FOUND: UserErrorCode.UserNotFound, + MULTIPLE_USER_MATCHES: UserErrorCode.MultipleUserMatches, + USER_ADD_FAILED: UserErrorCode.UserAddFailed, + USER_UPDATE_FAILED: UserErrorCode.UserUpdateFailed, + USER_DELETE_FAILED: UserErrorCode.UserDeleteFailed, + NO_USERS_TO_ACTION: UserErrorCode.NoUsersToAction, + USER_ACTION_FAILED: UserErrorCode.UserActionFailed, + USER_ACTION_ALL_NOT_SUPPORTED: UserErrorCode.UserActionAllNotSupported, + USER_ALIAS_FAILED: UserErrorCode.UserAliasFailed, + NO_TEAMS_FOR_USER_OP: UserErrorCode.NoTeamsForUserOp, + TEAM_USER_ADD_FAILED: UserErrorCode.TeamUserAddFailed, + TEAM_USER_REMOVE_FAILED: UserErrorCode.TeamUserRemoveFailed, + SYNC_FAILED: SyncErrorCode.SyncFailed, +} as const; export const KEEPER_PUBLIC_HOSTS: Record = { - US: 'keepersecurity.com', - EU: 'keepersecurity.eu', - AU: 'keepersecurity.com.au', - CA: 'keepersecurity.ca', - JP: 'keepersecurity.jp', - GOV: 'govcloud.keepersecurity.us', -} \ No newline at end of file + US: "keepersecurity.com", + EU: "keepersecurity.eu", + AU: "keepersecurity.com.au", + CA: "keepersecurity.ca", + JP: "keepersecurity.jp", + GOV: "govcloud.keepersecurity.us", +}; diff --git a/KeeperSdk/src/utils/errors.ts b/KeeperSdk/src/utils/errors.ts index b8f6622c..addefad8 100644 --- a/KeeperSdk/src/utils/errors.ts +++ b/KeeperSdk/src/utils/errors.ts @@ -1,108 +1,114 @@ -import type { KeeperError } from '@keeper-security/keeperapi' +import type { KeeperError } from "@keeper-security/keeperapi"; function parseJsonObjectIfPresent(s: string): Record | null { - const t = s.trim() - if (t.length === 0 || (t[0] !== '{' && t[0] !== '[')) return null - try { - const parsed: unknown = JSON.parse(s) - if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { - return parsed as Record - } - } catch { - /* not JSON */ + const t = s.trim(); + if (t.length === 0 || (t[0] !== "{" && t[0] !== "[")) return null; + try { + const parsed: unknown = JSON.parse(s); + if ( + typeof parsed === "object" && + parsed !== null && + !Array.isArray(parsed) + ) { + return parsed as Record; } - return null + } catch { + /* not JSON */ + } + return null; } export function isKeeperError(err: unknown): err is KeeperError { - return ( - err != null && - typeof err === 'object' && - !(err instanceof Error) && - ('result_code' in err || 'error' in err || 'response_code' in err) - ) + return ( + err != null && + typeof err === "object" && + !(err instanceof Error) && + ("result_code" in err || "error" in err || "response_code" in err) + ); } export function extractResultCode(err: unknown): string | undefined { - if (isKeeperError(err)) { - return err.result_code || err.error - } - if (err instanceof Error) { - const msg = err.message - if (msg.length > 0 && (msg[0] === '{' || msg[0] === '[')) { - try { - const parsed = JSON.parse(msg) - return parsed.result_code || parsed.error - } catch {} - } + if (isKeeperError(err)) { + return err.result_code || err.error; + } + if (err instanceof Error) { + const msg = err.message; + if (msg.length > 0 && (msg[0] === "{" || msg[0] === "[")) { + try { + const parsed = JSON.parse(msg); + return parsed.result_code || parsed.error; + } catch {} } - if (typeof err === 'string') { - const parsed = parseJsonObjectIfPresent(err) - if (parsed) { - if (typeof parsed.result_code === 'string') return parsed.result_code - if (typeof parsed.error === 'string') return parsed.error - } - return err + } + if (typeof err === "string") { + const parsed = parseJsonObjectIfPresent(err); + if (parsed) { + if (typeof parsed.result_code === "string") return parsed.result_code; + if (typeof parsed.error === "string") return parsed.error; } - if (typeof err === 'object' && err !== null) { - const obj = err as Record - if (typeof obj.result_code === 'string') return obj.result_code - if (typeof obj.error === 'string') return obj.error - } - return undefined + return err; + } + if (typeof err === "object" && err !== null) { + const obj = err as Record; + if (typeof obj.result_code === "string") return obj.result_code; + if (typeof obj.error === "string") return obj.error; + } + return undefined; } export function extractErrorMessage(err: unknown): string { - if (isKeeperError(err)) { - return err.message || err.result_code || err.error || 'Unknown Keeper error' - } - if (err instanceof Error) { - const parsed = parseJsonObjectIfPresent(err.message) - if (parsed) { - if (typeof parsed.message === 'string') return parsed.message - if (typeof parsed.result_code === 'string') return parsed.result_code - if (typeof parsed.error === 'string') return parsed.error - } - return err.message - } - if (typeof err === 'string') { - const parsed = parseJsonObjectIfPresent(err) - if (parsed) { - if (typeof parsed.message === 'string') return parsed.message - if (typeof parsed.result_code === 'string') return parsed.result_code - if (typeof parsed.error === 'string') return parsed.error - } - return err + if (isKeeperError(err)) { + return ( + err.message || err.result_code || err.error || "Unknown Keeper error" + ); + } + if (err instanceof Error) { + const parsed = parseJsonObjectIfPresent(err.message); + if (parsed) { + if (typeof parsed.message === "string") return parsed.message; + if (typeof parsed.result_code === "string") return parsed.result_code; + if (typeof parsed.error === "string") return parsed.error; } - if (typeof err === 'object' && err !== null) { - const obj = err as Record - if (typeof obj.message === 'string') return obj.message - if (typeof obj.result_code === 'string') return obj.result_code + return err.message; + } + if (typeof err === "string") { + const parsed = parseJsonObjectIfPresent(err); + if (parsed) { + if (typeof parsed.message === "string") return parsed.message; + if (typeof parsed.result_code === "string") return parsed.result_code; + if (typeof parsed.error === "string") return parsed.error; } - return String(err) + return err; + } + if (typeof err === "object" && err !== null) { + const obj = err as Record; + if (typeof obj.message === "string") return obj.message; + if (typeof obj.result_code === "string") return obj.result_code; + } + return String(err); } export class KeeperSdkError extends Error { - readonly resultCode?: string - readonly keeperError?: KeeperError + readonly resultCode?: string; + readonly keeperError?: KeeperError; - constructor(message: string, resultCode?: string, keeperError?: KeeperError) { - super(message) - this.name = 'KeeperSdkError' - this.resultCode = resultCode - this.keeperError = keeperError - } + constructor(message: string, resultCode?: string, keeperError?: KeeperError) { + super(message); + this.name = "KeeperSdkError"; + this.resultCode = resultCode; + this.keeperError = keeperError; + } - static from(err: unknown): KeeperSdkError { - if (err instanceof KeeperSdkError) return err - if (isKeeperError(err)) { - return new KeeperSdkError( - err.message || err.result_code || err.error || 'Unknown Keeper error', - err.result_code || err.error, - err - ) - } - if (err instanceof Error) return new KeeperSdkError(err.message) - return new KeeperSdkError(extractErrorMessage(err)) + static from(err: unknown): KeeperSdkError { + if (err instanceof KeeperSdkError) return err; + if (isKeeperError(err)) { + return new KeeperSdkError( + err.message || err.result_code || err.error || "Unknown Keeper error", + err.result_code || err.error, + err, + ); } + if (err instanceof Error) return new KeeperSdkError(err.message); + return new KeeperSdkError(extractErrorMessage(err)); + } } diff --git a/KeeperSdk/src/utils/guards.ts b/KeeperSdk/src/utils/guards.ts index 972f7769..b2ba9499 100644 --- a/KeeperSdk/src/utils/guards.ts +++ b/KeeperSdk/src/utils/guards.ts @@ -1,24 +1,24 @@ export function isBoolean(value: unknown): value is boolean { - return typeof value === 'boolean' + return typeof value === "boolean"; } export function isString(value: unknown): value is string { - return typeof value === 'string' + return typeof value === "string"; } export function isNonEmptyString(value: unknown): value is string { - return typeof value === 'string' && value.trim().length > 0 + return typeof value === "string" && value.trim().length > 0; } export function isNumber(value: unknown): value is number { - return typeof value === 'number' && Number.isFinite(value) + return typeof value === "number" && Number.isFinite(value); } export function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) + return typeof value === "object" && value !== null && !Array.isArray(value); } /** True if at least one of the values is a boolean (true or false). */ export function anyIsBoolean(...values: unknown[]): boolean { - return values.some(isBoolean) + return values.some(isBoolean); } diff --git a/KeeperSdk/src/utils/index.ts b/KeeperSdk/src/utils/index.ts index 7b53cf56..ea0bfb9f 100644 --- a/KeeperSdk/src/utils/index.ts +++ b/KeeperSdk/src/utils/index.ts @@ -1,28 +1,48 @@ export { - SdkDefaults, - AuthDefaults, - ResultCodes, - AuthErrorCode, - SessionErrorCode, - ValidationErrorCode, - RoleErrorCode, - TeamErrorCode, - UserErrorCode, - KEEPER_PUBLIC_HOSTS, -} from './constants' -export { Logger, ConsoleLogger, LogLevel, logger, setLogger, getLogger, resetLogger } from './Logger' -export type { ILogger } from './Logger' -export { KeeperSdkError, isKeeperError, extractErrorMessage, extractResultCode } from './errors' -export type { Nullable, Optional, DeepPartial, Immutable } from './types' -export { isBoolean, isString, isNonEmptyString, isNumber, isObject, anyIsBoolean } from './guards' + SdkDefaults, + AuthDefaults, + ResultCodes, + AuthErrorCode, + SessionErrorCode, + ValidationErrorCode, + RoleErrorCode, + TeamErrorCode, + UserErrorCode, + KEEPER_PUBLIC_HOSTS, +} from "./constants"; export { - EMAIL_PATTERN, - EMAIL_LIST_SEPARATOR_PATTERN, - TOKEN_SEPARATOR_PATTERN, - REGEX_ESCAPE_PATTERN, - TRAILING_EQUALS_PATTERN, - WHITESPACE_PATTERN, - isValidEmail, - escapeRegExp, - resolveSearchPattern, -} from './patterns' + Logger, + ConsoleLogger, + LogLevel, + logger, + setLogger, + getLogger, + resetLogger, +} from "./Logger"; +export type { ILogger } from "./Logger"; +export { + KeeperSdkError, + isKeeperError, + extractErrorMessage, + extractResultCode, +} from "./errors"; +export type { Nullable, Optional, DeepPartial, Immutable } from "./types"; +export { + isBoolean, + isString, + isNonEmptyString, + isNumber, + isObject, + anyIsBoolean, +} from "./guards"; +export { + EMAIL_PATTERN, + EMAIL_LIST_SEPARATOR_PATTERN, + TOKEN_SEPARATOR_PATTERN, + REGEX_ESCAPE_PATTERN, + TRAILING_EQUALS_PATTERN, + WHITESPACE_PATTERN, + isValidEmail, + escapeRegExp, + resolveSearchPattern, +} from "./patterns"; diff --git a/KeeperSdk/src/utils/patterns.ts b/KeeperSdk/src/utils/patterns.ts index fe15b54c..a4468bf4 100644 --- a/KeeperSdk/src/utils/patterns.ts +++ b/KeeperSdk/src/utils/patterns.ts @@ -1,36 +1,39 @@ -import { KeeperSdkError } from './errors' -import { ResultCodes } from './constants' +import { KeeperSdkError } from "./errors"; +import { ResultCodes } from "./constants"; -export const EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+\.[^\s@]+$/ +export const EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+\.[^\s@]+$/; /** Splits user-entered email lists on whitespace, commas, and semicolons. */ -export const EMAIL_LIST_SEPARATOR_PATTERN = /[\s,;]+/ +export const EMAIL_LIST_SEPARATOR_PATTERN = /[\s,;]+/; /** Splits free-form text into search tokens on whitespace and common punctuation. */ -export const TOKEN_SEPARATOR_PATTERN = /[\s\-_.,;:!?@#$%^&*()[\]{}|\\/<>]+/ +export const TOKEN_SEPARATOR_PATTERN = /[\s\-_.,;:!?@#$%^&*()[\]{}|\\/<>]+/; /** Characters that must be escaped when embedding user input into a RegExp. */ -export const REGEX_ESCAPE_PATTERN = /[.+^${}()|[\]\\]/g +export const REGEX_ESCAPE_PATTERN = /[.+^${}()|[\]\\]/g; /** Sequence of one or more `=` characters at end of string (Base32 padding). */ -export const TRAILING_EQUALS_PATTERN = /=+$/g +export const TRAILING_EQUALS_PATTERN = /=+$/g; /** Any whitespace run. */ -export const WHITESPACE_PATTERN = /\s+/g +export const WHITESPACE_PATTERN = /\s+/g; export function isValidEmail(value: string): boolean { - return EMAIL_PATTERN.test(value) + return EMAIL_PATTERN.test(value); } export function escapeRegExp(value: string): string { - return value.replace(REGEX_ESCAPE_PATTERN, '\\$&') + return value.replace(REGEX_ESCAPE_PATTERN, "\\$&"); } export function resolveSearchPattern(pattern: unknown): string | null { - if (pattern == null) return null - if (typeof pattern !== 'string') { - throw new KeeperSdkError('Pattern must be a string.', ResultCodes.INVALID_PATTERN) - } - const trimmed = pattern.trim() - return trimmed.length > 0 ? trimmed : null + if (pattern == null) return null; + if (typeof pattern !== "string") { + throw new KeeperSdkError( + "Pattern must be a string.", + ResultCodes.INVALID_PATTERN, + ); + } + const trimmed = pattern.trim(); + return trimmed.length > 0 ? trimmed : null; } diff --git a/KeeperSdk/src/utils/types.ts b/KeeperSdk/src/utils/types.ts index 02113ea6..fde5ac3d 100644 --- a/KeeperSdk/src/utils/types.ts +++ b/KeeperSdk/src/utils/types.ts @@ -1,11 +1,13 @@ /** A value of type T or null. Use this for caches and computed-but-empty results. */ -export type Nullable = T | null +export type Nullable = T | null; /** A value of type T or undefined. Use this for "not provided" inputs. */ -export type Optional = T | undefined +export type Optional = T | undefined; /** Recursively makes every property optional. */ -export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T +export type DeepPartial = T extends object + ? { [K in keyof T]?: DeepPartial } + : T; /** Helper to mark properties as readonly. */ -export type Immutable = { readonly [K in keyof T]: T[K] } +export type Immutable = { readonly [K in keyof T]: T[K] }; diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 8248dd0f..f779ca50 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -1,846 +1,970 @@ import { - Auth, - KeeperEnvironment, - syncDown, - DRecord, - DRecordMetadata, - DSharedFolder, - DTeam, - DUserFolder, - Authentication, - normal64Bytes, -} from '@keeper-security/keeperapi' -import type { SyncResult, SyncLogFormat, VaultStorage, SessionStorage, AuthUI3 } from '@keeper-security/keeperapi' -import { InMemoryStorage } from '../storage/InMemoryStorage' -import { SessionManager } from '../auth/SessionManager' -import { getSdkPlatform } from '../platform' + Auth, + KeeperEnvironment, + syncDown, + DRecord, + DRecordMetadata, + DSharedFolder, + DTeam, + DUserFolder, + Authentication, + normal64Bytes, +} from "@keeper-security/keeperapi"; +import type { + SyncResult, + SyncLogFormat, + VaultStorage, + SessionStorage, + AuthUI3, +} from "@keeper-security/keeperapi"; +import { InMemoryStorage } from "../storage/InMemoryStorage"; +import { SessionManager } from "../auth/SessionManager"; +import { getSdkPlatform } from "../platform"; +import { + toSessionParams, + type SessionRestoreInput, +} from "../auth/sessionRestore"; import { - toSessionParams, - type SessionRestoreInput, -} from '../auth/sessionRestore' -import { searchRecords, formatRecord, getRecordTitle, getRecordType } from '../records/RecordUtils' + searchRecords, + formatRecord, + getRecordTitle, + getRecordType, +} from "../records/RecordUtils"; import { - addRecord as addRecordOp, - updateRecord as updateRecordOp, - deleteRecord as deleteRecordOp, - getRecordHistory as getRecordHistoryOp, - moveRecord as moveRecordOp, -} from '../records/RecordOperations' + addRecord as addRecordOp, + updateRecord as updateRecordOp, + deleteRecord as deleteRecordOp, + getRecordHistory as getRecordHistoryOp, + moveRecord as moveRecordOp, +} from "../records/RecordOperations"; import type { - NewRecordInput, - TypedRecordData, - AddRecordResult, - UpdateRecordResult, - DeleteRecordResult, - RecordHistoryResult, - MoveRecordInput, - MoveRecordResult, -} from '../records/RecordOperations' + NewRecordInput, + TypedRecordData, + AddRecordResult, + UpdateRecordResult, + DeleteRecordResult, + RecordHistoryResult, + MoveRecordInput, + MoveRecordResult, +} from "../records/RecordOperations"; import { - shareRecord as shareRecordOp, - removeRecordShare as removeRecordShareOp, - getRecordShareInfo as getRecordShareInfoOp, -} from '../sharing/Sharing' + shareRecord as shareRecordOp, + removeRecordShare as removeRecordShareOp, + getRecordShareInfo as getRecordShareInfoOp, +} from "../sharing/Sharing"; +import type { + ShareRecordInput, + ShareRecordResult, + RemoveShareInput, + RemoveShareResult, + RecordShareInfo, +} from "../sharing/Sharing"; +import type { + ListFolderOptions, + ListFolderResult, +} from "../folders/listFolder"; +import { FolderKind, VaultObjectKind } from "../folders/folderHelpers"; +import type { + ChangeDirectoryResult, + VaultFolderSession, +} from "../folders/changeDirectory"; +import type { + AddFolderInput, + AddFolderResult, + MkdirOptions, +} from "../folders/addFolder"; import type { - ShareRecordInput, - ShareRecordResult, - RemoveShareInput, - RemoveShareResult, - RecordShareInfo, -} from '../sharing/Sharing' -import type { ListFolderOptions, ListFolderResult } from '../folders/listFolder' -import { FolderKind, VaultObjectKind } from '../folders/folderHelpers' -import type { ChangeDirectoryResult, VaultFolderSession } from '../folders/changeDirectory' -import type { AddFolderInput, AddFolderResult, MkdirOptions } from '../folders/addFolder' -import type { UpdateFolderInput, UpdateFolderResult, RenameFolderResult } from '../folders/updateFolder' -import type { DeleteFolderResult, RmdirOptions } from '../folders/deleteFolder' -import type { FolderTreeBuildOptions } from '../folders/folderTree' -import type { GetFolderOptions, GetFolderResult } from '../folders/getFolder' -import type { ListSharedFolderRow, ListSharedFoldersOptions } from '../sharedFolders/listSharedFolders' -import type { ShareFolderInput, ShareFolderResult } from '../sharedFolders/shareFolder' -import { FolderManager } from '../folders/FolderManager' -import type { SharedFolderPermissionsInput } from '../folders/FolderManager' -import { SharedFolderManager } from '../sharedFolders/SharedFolderManager' -import { TeamManager } from '../teams/TeamManager' -import type { ListTeamRow, ListTeamsOptions } from '../teams/listTeams' -import type { TeamView } from '../teams/viewTeam' -import type { AddTeamInput, AddTeamResult } from '../teams/addTeam' -import type { UpdateTeamInput, UpdateTeamResult } from '../teams/updateTeam' -import type { DeleteTeamInput, DeleteTeamResult } from '../teams/deleteTeam' + UpdateFolderInput, + UpdateFolderResult, + RenameFolderResult, +} from "../folders/updateFolder"; +import type { DeleteFolderResult, RmdirOptions } from "../folders/deleteFolder"; +import type { FolderTreeBuildOptions } from "../folders/folderTree"; +import type { GetFolderOptions, GetFolderResult } from "../folders/getFolder"; +import type { + ListSharedFolderRow, + ListSharedFoldersOptions, +} from "../sharedFolders/listSharedFolders"; +import type { + ShareFolderInput, + ShareFolderResult, +} from "../sharedFolders/shareFolder"; +import { FolderManager } from "../folders/FolderManager"; +import type { SharedFolderPermissionsInput } from "../folders/FolderManager"; +import { SharedFolderManager } from "../sharedFolders/SharedFolderManager"; +import { TeamManager } from "../teams/TeamManager"; +import type { ListTeamRow, ListTeamsOptions } from "../teams/listTeams"; +import type { TeamView } from "../teams/viewTeam"; +import type { AddTeamInput, AddTeamResult } from "../teams/addTeam"; +import type { UpdateTeamInput, UpdateTeamResult } from "../teams/updateTeam"; +import type { DeleteTeamInput, DeleteTeamResult } from "../teams/deleteTeam"; import { - RoleManager, - type AddRoleInput, - type AddRoleResult, - type DeleteRoleInput, - type DeleteRoleResult, - type ListRoleRow, - type ListRolesOptions, - type RoleView, - type UpdateRoleInput, - type UpdateRoleResult, -} from '../roles' -import { UserManager } from '../users/UserManager' + RoleManager, + type AddRoleInput, + type AddRoleResult, + type DeleteRoleInput, + type DeleteRoleResult, + type ListRoleRow, + type ListRolesOptions, + type RoleView, + type UpdateRoleInput, + type UpdateRoleResult, +} from "../roles"; +import { UserManager } from "../users/UserManager"; import type { - ListUserRow, - ListUsersOptions, - UserView, - AddUserInput, - AddUserResult, - FormatAddUserResultOptions, - FormattedAddUserTable, - UpdateUserInput, - UpdateUserResult, - FormattedUpdateUserTable, - DeleteUserInput, - DeleteUserResult, - FormattedDeleteUserTable, - UserActionInput, - UserActionResult, - FormattedUserActionTable, - AliasUserInput, - AliasUserResult, - AddUsersToTeamsInput, - RemoveUsersFromTeamsInput, - TeamUserResult, - FormattedTeamUserTable, -} from '../users/userTypes' + ListUserRow, + ListUsersOptions, + UserView, + AddUserInput, + AddUserResult, + FormatAddUserResultOptions, + FormattedAddUserTable, + UpdateUserInput, + UpdateUserResult, + FormattedUpdateUserTable, + DeleteUserInput, + DeleteUserResult, + FormattedDeleteUserTable, + UserActionInput, + UserActionResult, + FormattedUserActionTable, + AliasUserInput, + AliasUserResult, + AddUsersToTeamsInput, + RemoveUsersFromTeamsInput, + TeamUserResult, + FormattedTeamUserTable, +} from "../users/userTypes"; import { - ConsoleLogger, - LogLevel, - KeeperSdkError, - extractErrorMessage, - extractResultCode, - SdkDefaults, - ResultCodes, -} from '../utils' -import type { ILogger } from '../utils' + ConsoleLogger, + LogLevel, + KeeperSdkError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + ResultCodes, +} from "../utils"; +import type { ILogger } from "../utils"; enum VaultStatus { - RecordNotFound = 'RECORD_NOT_FOUND', - RecordKeyNotFound = 'RECORD_KEY_NOT_FOUND', + RecordNotFound = "RECORD_NOT_FOUND", + RecordKeyNotFound = "RECORD_KEY_NOT_FOUND", } export type KeeperVaultConfig = { - host?: string - clientVersion?: string - configDir?: string - useConsoleAuth?: boolean - logFormat?: SyncLogFormat - logLevel?: LogLevel - autoSync?: boolean - storage?: InMemoryStorage - sessionStorage?: SessionManager - authUI?: AuthUI3 -} + host?: string; + clientVersion?: string; + configDir?: string; + useConsoleAuth?: boolean; + logFormat?: SyncLogFormat; + logLevel?: LogLevel; + autoSync?: boolean; + storage?: InMemoryStorage; + sessionStorage?: SessionManager; + authUI?: AuthUI3; +}; export type VaultSummary = { - recordCount: number - sharedFolderCount: number - teamCount: number - folderCount: number -} + recordCount: number; + sharedFolderCount: number; + teamCount: number; + folderCount: number; +}; export class KeeperVault { - private auth: Auth | null = null - private readonly storage: InMemoryStorage - private readonly sessionManager: SessionManager - private readonly authUI: AuthUI3 - private readonly config: Required> - private readonly log: ILogger - private synced = false - private batchDepth = 0 - private readonly folderSession: VaultFolderSession = FolderManager.createSession() - private readonly folderManager: FolderManager - private readonly sharedFolderManager: SharedFolderManager - private readonly teamManager: TeamManager - private readonly roleManager: RoleManager - private readonly userManager: UserManager - - constructor(config?: KeeperVaultConfig) { - this.config = { - host: config?.host || KeeperEnvironment.Prod, - clientVersion: config?.clientVersion || SdkDefaults.CLIENT_VERSION, - configDir: config?.configDir ?? '', - useConsoleAuth: config?.useConsoleAuth !== false, - logFormat: config?.logFormat || SdkDefaults.LOG_FORMAT, - logLevel: config?.logLevel ?? LogLevel.INFO, - autoSync: config?.autoSync !== false, - } - - this.log = new ConsoleLogger(this.config.logLevel) - this.storage = config?.storage || new InMemoryStorage() - this.sessionManager = config?.sessionStorage || new SessionManager(this.config.configDir || undefined) - this.authUI = config?.authUI ?? getSdkPlatform().createAuthUI(this.config.useConsoleAuth) - - const authProvider = () => this.getAuthOrThrow() - this.folderManager = new FolderManager(this.storage, this.folderSession, authProvider) - this.sharedFolderManager = new SharedFolderManager(this.storage, authProvider) - this.teamManager = new TeamManager(authProvider) - this.roleManager = new RoleManager(authProvider) - this.userManager = new UserManager(authProvider) - } - - public getFolderManager(): FolderManager { - return this.folderManager - } - - public getSharedFolderManager(): SharedFolderManager { - return this.sharedFolderManager - } - - public getTeamManager(): TeamManager { - return this.teamManager - } - - public getRoleManager(): RoleManager { - return this.roleManager - } - - private async createAuth(options?: { useSessionResumption?: boolean }): Promise { - const host = this.config.host - const baseDeviceConfig = await this.sessionManager.getDeviceConfig(host) - const deviceConfig = { - ...baseDeviceConfig, - deviceName: baseDeviceConfig.deviceName || SdkDefaults.DEVICE_NAME, - } - - const sessionStorage: SessionStorage = - options?.useSessionResumption === false - ? { - getCloneCode: async () => null, - saveCloneCode: (h, u, c) => this.sessionManager.saveCloneCode(h, u, c), - getSessionParameters: () => this.sessionManager.getSessionParameters(), - saveSessionParameters: (p) => this.sessionManager.saveSessionParameters(p), - } - : this.sessionManager - - return new Auth({ - host, - clientVersion: this.config.clientVersion, - deviceConfig, - authUI3: this.config.useConsoleAuth ? this.authUI : undefined, - sessionStorage, - onDeviceConfig: this.sessionManager.createOnDeviceConfig(host), - useSessionResumption: options?.useSessionResumption, - }) - } - - private getAuthOrThrow(): Auth { - if (!this.auth || !this.auth.sessionToken) { - throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN) - } - return this.auth - } - - public async login(username: string, password: string): Promise { - this.auth = await this.createAuth({ useSessionResumption: false }) - this.sessionManager.setLastUsername(username) - - await this.auth.loginV3({ - username, - password, - loginType: Authentication.LoginType.NORMAL, - loginMethod: Authentication.LoginMethod.EXISTING_ACCOUNT, - }) - - this.synced = false - this.log.info(`Logged in as ${username}`) - } - - public async loginWithSessionToken(username: string, sessionToken: string): Promise { - const deviceConfig = await this.sessionManager.getDeviceConfig(this.config.host) - - if (!deviceConfig.deviceToken || !deviceConfig.privateKey) { - throw new KeeperSdkError( - 'Device is not registered for this host. Perform a normal login first to register the device before using session token login.', - ResultCodes.DEVICE_NOT_REGISTERED - ) - } - - this.auth = await this.createAuth() - this.sessionManager.setLastUsername(username) - - await this.auth.loginV3({ - username, - givenSessionToken: sessionToken, - loginType: Authentication.LoginType.NORMAL, - loginMethod: Authentication.LoginMethod.EXISTING_ACCOUNT, - }) - - if (!this.auth.sessionToken) { - throw new KeeperSdkError( - 'Session token login failed — token may be expired or invalid.', - ResultCodes.SESSION_TOKEN_EXPIRED - ) - } - - this.synced = false - this.log.info(`Logged in as ${username} (via session token)`) - } - - /** - * Persist device token and private key for this vault host in session storage so - * {@link loginWithSessionToken} and {@link resumeSession} work without a prior password login on this machine. - * Values use the same base64 / base64url decoding as {@link SessionManager} (`normal64Bytes`). - */ - public async registerDevice( - deviceToken: string, - privateKey: string, - options?: { username?: string } - ): Promise { - const host = this.config.host - const save = this.sessionManager.createOnDeviceConfig(host) - await save({ - deviceToken: normal64Bytes(deviceToken), - privateKey: normal64Bytes(privateKey), - }) - if (options?.username) { - this.sessionManager.setLastUsername(options.username) - } - this.log.info(`Device credentials stored for host ${host}`) - } - - public getSessionToken(): string | undefined { - return this.auth?.sessionToken || undefined - } - - /** - * Resume a session from extension-exported {@link SessionRestoreInput}. - * Verifies the token with a lightweight server call so an expired session - * fails here, not later from `sync()`. - */ - public async restoreSession(input: SessionRestoreInput): Promise { - const params = toSessionParams(input) - await this.sessionManager.saveSessionParameters(params) - this.sessionManager.setLastUsername(params.username) - - this.auth = await this.createAuth() - await this.auth.continueSession() - - if (!this.auth.sessionToken) { - throw new KeeperSdkError( - 'Session restore failed — session token may be expired or invalid.', - ResultCodes.SESSION_TOKEN_EXPIRED - ) - } - - try { - await this.auth.loadAccountSummary() - } catch (err) { - const code = extractResultCode(err) - const msg = extractErrorMessage(err) - this.disconnect() - const isExpired = code === ResultCodes.SESSION_TOKEN_EXPIRED - throw new KeeperSdkError( - isExpired - ? `Session token rejected by server (${code}): ${msg}. Re-export session JSON and try again.` - : `Session restore failed: ${msg}`, - code ?? ResultCodes.SESSION_TOKEN_EXPIRED - ) - } - - this.synced = false - this.log.info(`Session restored for ${params.username}`) - } - - public async getAccountUsername(): Promise { - return this.sessionManager.getLastUsername() - } - - public async resumeSession(): Promise { - const username = await this.sessionManager.getLastUsername() - if (!username) { - throw new KeeperSdkError( - 'No previous login found. Perform a normal login first.', - ResultCodes.NO_PREVIOUS_LOGIN - ) - } - - this.sessionManager.setLastUsername(username) - - const deviceConfig = await this.sessionManager.getDeviceConfig(this.config.host) - if (!deviceConfig.deviceToken || !deviceConfig.privateKey) { - throw new KeeperSdkError( - 'Device is not registered for this host. Perform a normal login first.', - ResultCodes.DEVICE_NOT_REGISTERED - ) - } - - const cloneCode = await this.sessionManager.getCloneCode(this.config.host, username) - if (!cloneCode) { - throw new KeeperSdkError( - 'No clone code found. Persistent login not enabled or clone code expired. Perform a normal login.', - ResultCodes.NO_CLONE_CODE - ) - } - - this.auth = await this.createAuth({ useSessionResumption: true }) - - await this.auth.loginV3({ - loginType: Authentication.LoginType.NORMAL, - resumeSessionOnly: true, - }) - - if (!this.auth.sessionToken) { - throw new KeeperSdkError( - 'Persistent login failed — clone code may be expired or persistent login not enabled. Perform a normal login.', - ResultCodes.PERSISTENT_LOGIN_FAILED - ) - } - - this.synced = false - this.log.info(`Session resumed for ${username} (persistent login)`) - } - - public async sync(): Promise { - const auth = this.getAuthOrThrow() - - try{ - const result = await syncDown({ - auth, - storage: this.storage, - logFormat: this.config.logFormat, - }) - if (result.error) { - this.log.error('Sync error:', result.error) - throw new KeeperSdkError(`Sync failed: ${result.error}`, ResultCodes.SYNC_FAILED) - } - this.synced = true - return result - }catch(e) { - this.log.error('Sync failed:', extractErrorMessage(e)) - throw e - } - } - - public async batch(fn: () => Promise): Promise { - this.batchDepth++ - try { - await fn() - } finally { - this.batchDepth-- - if (this.batchDepth === 0 && this.config.autoSync) { - await this.sync() - } - } - } - - private async syncIfNeeded(): Promise { - if (this.batchDepth > 0) { - this.synced = false - return - } - if (this.config.autoSync) { - await this.sync() - } else { - this.synced = false - } - } - - public getRecords(): DRecord[] { - return this.storage.getRecords() - } - - public getRecordByUid(uid: string): DRecord | undefined { - return this.storage.getByUid(VaultObjectKind.Record, uid) - } - - public findRecord(uidOrTitle: string): DRecord | undefined { - const byUid = this.getRecordByUid(uidOrTitle) - if (byUid) return byUid - - const lowerUidOrTitle = uidOrTitle.toLowerCase() - return this.getRecords().find((record) => getRecordTitle(record).toLowerCase() === lowerUidOrTitle) - } - - public findRecords(criteria: string): DRecord[] { - return searchRecords(this.getRecords(), criteria) - } - - public getRecordsByVersion(version: number): DRecord[] { - return this.getRecords().filter((record) => record.version === version) - } - - public getRecordsByType(recordType: string): DRecord[] { - return this.getRecords().filter((record) => getRecordType(record) === recordType) - } - - public getRecordMetadata(): DRecordMetadata[] { - return this.storage.getAll(VaultObjectKind.Metadata) - } - - public getRecordMetadataByUid(uid: string): DRecordMetadata | undefined { - return this.storage.getByUid(VaultObjectKind.Metadata, uid) - } - - public getSharedFolders(): DSharedFolder[] { - return this.storage.getAll(FolderKind.SharedFolder) - } - - public getTeams(): DTeam[] { - return this.storage.getAll(VaultObjectKind.Team) - } - - public getUserFolders(): DUserFolder[] { - return this.storage.getAll(FolderKind.UserFolder) - } - - public async listFolder(options?: ListFolderOptions): Promise { - return this.folderManager.listFolder(options ?? {}) - } - - public listSharedFolders(options?: ListSharedFoldersOptions): ListSharedFolderRow[] { - return this.sharedFolderManager.listSharedFolders(options ?? {}) - } - - public async listTeams(options?: ListTeamsOptions): Promise { - return this.teamManager.listTeams(options ?? {}) - } - - public async listUsers(options?: ListUsersOptions): Promise { - return this.userManager.listUsers(options ?? {}) - } - - public async viewUser(identifier: string): Promise { - return this.userManager.viewUser(identifier) - } - - public async addUsers(input: AddUserInput): Promise { - return this.userManager.addUsers(input) - } - - public formatAddUserResult(result: AddUserResult, options: FormatAddUserResultOptions = {}): FormattedAddUserTable { - return this.userManager.formatAddUserResult(result, options) - } - - public async updateUsers(input: UpdateUserInput): Promise { - return this.userManager.updateUsers(input) - } - - public formatUpdateUserResult(result: UpdateUserResult): FormattedUpdateUserTable { - return this.userManager.formatUpdateUserResult(result) - } - - public async deleteUsers(input: DeleteUserInput): Promise { - return this.userManager.deleteUsers(input) - } - - public formatDeleteUserResult(result: DeleteUserResult): FormattedDeleteUserTable { - return this.userManager.formatDeleteUserResult(result) - } - - public async actionUsers(input: UserActionInput): Promise { - return this.userManager.actionUsers(input) - } - - public formatUserActionResult(result: UserActionResult): FormattedUserActionTable { - return this.userManager.formatUserActionResult(result) - } - - public async aliasUser(input: AliasUserInput): Promise { - return this.userManager.aliasUser(input) - } - - public async addUsersToTeams(input: AddUsersToTeamsInput): Promise { - return this.userManager.addUsersToTeams(input) - } - - public async removeUsersFromTeams(input: RemoveUsersFromTeamsInput): Promise { - return this.userManager.removeUsersFromTeams(input) - } - - public formatTeamUserResult(result: TeamUserResult): FormattedTeamUserTable { - return this.userManager.formatTeamUserResult(result) - } - - public async viewTeam(identifier: string): Promise { - return this.teamManager.viewTeam(identifier) - } - - public async addTeams(input: AddTeamInput): Promise { - const result = await this.teamManager.addTeams(input) - if (result.created > 0) await this.syncIfNeeded() - return result - } - - public async updateTeams(input: UpdateTeamInput): Promise { - const result = await this.teamManager.updateTeams(input) - if (result.updated > 0) await this.syncIfNeeded() - return result - } - - public async deleteTeams(input: DeleteTeamInput): Promise { - const result = await this.teamManager.deleteTeams(input) - if (result.deleted > 0) await this.syncIfNeeded() - return result - } - - public async listRoles(options?: ListRolesOptions): Promise { - return this.roleManager.listRoles(options ?? {}) - } - - public async viewRole(identifier: string): Promise { - return this.roleManager.viewRole(identifier) - } - - public async addRoles(input: AddRoleInput): Promise { - const result = await this.roleManager.addRoles(input) - if (result.created > 0) await this.syncIfNeeded() - return result - } - - public async updateRoles(input: UpdateRoleInput): Promise { - const result = await this.roleManager.updateRoles(input) - if (result.updated > 0) await this.syncIfNeeded() - return result - } - - public async deleteRoles(input: DeleteRoleInput): Promise { - const result = await this.roleManager.deleteRoles(input) - if (result.deleted > 0) await this.syncIfNeeded() - return result - } - - public async changeDirectory(path: string): Promise { - return this.folderManager.changeDirectory(path) - } - - public getCurrentFolderUid(): string | null { - return this.folderManager.getCurrentFolderUid() - } - - public getWorkingFolderDisplayName(): string { - return this.folderManager.getWorkingFolderDisplayName() - } - - public async getFolder(uidOrName: string, options?: GetFolderOptions): Promise { - return this.folderManager.getFolder(uidOrName, options ?? {}) - } - - public async addFolder(input: AddFolderInput): Promise { - const result = await this.folderManager.addFolder(input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async mkdir( - path: string, - options?: MkdirOptions - ): Promise<{ folderUid: string; success: boolean; message?: string }> { - const result = await this.folderManager.mkdir(path, options ?? {}) - if (result.success) await this.syncIfNeeded() - return result - } - - public async updateFolder(input: UpdateFolderInput): Promise { - const result = await this.folderManager.updateFolder(input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async renameFolder(folderPath: string, newName: string): Promise { - const result = await this.folderManager.renameFolder(folderPath, newName) - if (result.success) await this.syncIfNeeded() - return result - } - - public async updateSharedFolderPermissions( - sharedFolderUid: string, - permissions: SharedFolderPermissionsInput - ): Promise { - const result = await this.folderManager.updateSharedFolderPermissions(sharedFolderUid, permissions) - if (result.success) await this.syncIfNeeded() - return result - } - - public async deleteFolder( - folderRefs: string[], - confirm?: (summary: string) => boolean | Promise - ): Promise { - const result = await this.folderManager.deleteFolder(folderRefs, confirm) - if (result.success) await this.syncIfNeeded() - return result - } - - public async rmdir(patterns: string[], options?: RmdirOptions): Promise { - const result = await this.folderManager.rmdir(patterns, options ?? {}) - if (result.success) await this.syncIfNeeded() - return result - } - - public async tree(options?: FolderTreeBuildOptions): Promise { - this.getAuthOrThrow() - return this.folderManager.folderTreeAscii(options ?? {}) - } - - public getSummary(): VaultSummary { - return { - recordCount: this.storage.getCount(VaultObjectKind.Record), - sharedFolderCount: this.storage.getCount(FolderKind.SharedFolder), - teamCount: this.storage.getCount(VaultObjectKind.Team), - folderCount: this.storage.getCount(FolderKind.UserFolder), - } - } - - public printRecords(showDetails = false): void { - const records = this.getRecords() - if (records.length === 0) { - this.log.info('No records found in vault.') - return - } - this.log.info(`\n=== Vault Records (${records.length}) ===\n`) - for (const record of records) { - this.log.info(formatRecord(record, showDetails)) - } - } - - public async addRecord(input: NewRecordInput): Promise { - const auth = this.getAuthOrThrow() - const result = await addRecordOp(auth, input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async updateRecord(recordUid: string, data: TypedRecordData): Promise { - const auth = this.getAuthOrThrow() - - const record = this.getRecordByUid(recordUid) - if (!record) { - return { recordUid, success: false, status: VaultStatus.RecordNotFound } - } - - const keyBytes = await this.storage.getKeyBytes(recordUid) - if (!keyBytes) { - return { - recordUid, - success: false, - status: VaultStatus.RecordKeyNotFound, - } - } - - const result = await updateRecordOp(auth, recordUid, data, record.revision, keyBytes) - if (result.success) await this.syncIfNeeded() - return result - } - - public async deleteRecord(recordUid: string): Promise { - const auth = this.getAuthOrThrow() - const result = await deleteRecordOp(auth, recordUid) - if (result.success) await this.syncIfNeeded() - return result - } - - public async moveRecord(input: MoveRecordInput): Promise { - const auth = this.getAuthOrThrow() - const result = await moveRecordOp(auth, this.storage, input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async shareRecord(input: ShareRecordInput): Promise { - const auth = this.getAuthOrThrow() - - const record = this.getRecordByUid(input.recordUid) || this.findRecord(input.recordUid) - if (!record) { - return { - recordUid: input.recordUid, - email: input.email, - success: false, - status: VaultStatus.RecordNotFound, - message: `Record "${input.recordUid}" not found`, - } - } - - const keyBytes = await this.storage.getKeyBytes(record.uid) - if (!keyBytes) { - return { - recordUid: record.uid, - email: input.email, - success: false, - status: VaultStatus.RecordKeyNotFound, - message: 'Record key not available', - } - } - - const result = await shareRecordOp(auth, keyBytes, { - ...input, - recordUid: record.uid, - }) - if (result.success) await this.syncIfNeeded() - return result - } - - public async removeRecordShare(input: RemoveShareInput): Promise { - const auth = this.getAuthOrThrow() - const result = await removeRecordShareOp(auth, input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async getRecordShareInfo(recordUid: string): Promise { - const auth = this.getAuthOrThrow() - return getRecordShareInfoOp(auth, recordUid) - } - - public async shareFolder(input: ShareFolderInput): Promise { - const result = await this.sharedFolderManager.shareFolder(input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async getRecordHistory(recordUid: string): Promise { - const auth = this.getAuthOrThrow() - - const keyBytes = await this.storage.getKeyBytes(recordUid) - if (!keyBytes) { - return { recordUid, history: [] } - } - - return getRecordHistoryOp(auth, recordUid, keyBytes) - } - - public getStorage(): VaultStorage { - return this.storage - } - - public getAuth(): Auth { - return this.getAuthOrThrow() - } - - public disconnect(): void { - if (this.auth) { - try { - this.auth.disconnect() - } catch (err) { - this.log.debug('disconnect error:', extractErrorMessage(err)) - } - this.auth = null - } - this.synced = false - this.folderSession.currentFolderUid = null - } - - public async logout(): Promise { - if (this.auth) { - try { - await this.auth.logout() - } catch (err) { - this.log.debug('logout error:', extractErrorMessage(err)) - } - } - this.disconnect() - this.log.info('Logged out.') - } - - public get host(): string { - return this.config.host - } - - public get isLoggedIn(): boolean { - return this.auth !== null && !!this.auth.sessionToken - } - - public get isSynced(): boolean { - return this.synced - } + private auth: Auth | null = null; + private readonly storage: InMemoryStorage; + private readonly sessionManager: SessionManager; + private readonly authUI: AuthUI3; + private readonly config: Required< + Omit + >; + private readonly log: ILogger; + private synced = false; + private batchDepth = 0; + private readonly folderSession: VaultFolderSession = + FolderManager.createSession(); + private readonly folderManager: FolderManager; + private readonly sharedFolderManager: SharedFolderManager; + private readonly teamManager: TeamManager; + private readonly roleManager: RoleManager; + private readonly userManager: UserManager; + + constructor(config?: KeeperVaultConfig) { + this.config = { + host: config?.host || KeeperEnvironment.Prod, + clientVersion: config?.clientVersion || SdkDefaults.CLIENT_VERSION, + configDir: config?.configDir ?? "", + useConsoleAuth: config?.useConsoleAuth !== false, + logFormat: config?.logFormat || SdkDefaults.LOG_FORMAT, + logLevel: config?.logLevel ?? LogLevel.INFO, + autoSync: config?.autoSync !== false, + }; + + this.log = new ConsoleLogger(this.config.logLevel); + this.storage = config?.storage || new InMemoryStorage(); + this.sessionManager = + config?.sessionStorage || + new SessionManager(this.config.configDir || undefined); + this.authUI = + config?.authUI ?? + getSdkPlatform().createAuthUI(this.config.useConsoleAuth); + + const authProvider = () => this.getAuthOrThrow(); + this.folderManager = new FolderManager( + this.storage, + this.folderSession, + authProvider, + ); + this.sharedFolderManager = new SharedFolderManager( + this.storage, + authProvider, + ); + this.teamManager = new TeamManager(authProvider); + this.roleManager = new RoleManager(authProvider); + this.userManager = new UserManager(authProvider); + } + + public getFolderManager(): FolderManager { + return this.folderManager; + } + + public getSharedFolderManager(): SharedFolderManager { + return this.sharedFolderManager; + } + + public getTeamManager(): TeamManager { + return this.teamManager; + } + + public getRoleManager(): RoleManager { + return this.roleManager; + } + + private async createAuth(options?: { + useSessionResumption?: boolean; + }): Promise { + const host = this.config.host; + const baseDeviceConfig = await this.sessionManager.getDeviceConfig(host); + const deviceConfig = { + ...baseDeviceConfig, + deviceName: baseDeviceConfig.deviceName || SdkDefaults.DEVICE_NAME, + }; + + const sessionStorage: SessionStorage = + options?.useSessionResumption === false + ? { + getCloneCode: async () => null, + saveCloneCode: (h, u, c) => + this.sessionManager.saveCloneCode(h, u, c), + getSessionParameters: () => + this.sessionManager.getSessionParameters(), + saveSessionParameters: (p) => + this.sessionManager.saveSessionParameters(p), + } + : this.sessionManager; + + return new Auth({ + host, + clientVersion: this.config.clientVersion, + deviceConfig, + authUI3: this.config.useConsoleAuth ? this.authUI : undefined, + sessionStorage, + onDeviceConfig: this.sessionManager.createOnDeviceConfig(host), + useSessionResumption: options?.useSessionResumption, + }); + } + + private getAuthOrThrow(): Auth { + if (!this.auth || !this.auth.sessionToken) { + throw new KeeperSdkError( + "Not logged in. Call login() first.", + ResultCodes.NOT_LOGGED_IN, + ); + } + return this.auth; + } + + public async login(username: string, password: string): Promise { + this.auth = await this.createAuth({ useSessionResumption: false }); + this.sessionManager.setLastUsername(username); + + await this.auth.loginV3({ + username, + password, + loginType: Authentication.LoginType.NORMAL, + loginMethod: Authentication.LoginMethod.EXISTING_ACCOUNT, + }); + + this.synced = false; + this.log.info(`Logged in as ${username}`); + } + + public async loginWithSessionToken( + username: string, + sessionToken: string, + ): Promise { + const deviceConfig = await this.sessionManager.getDeviceConfig( + this.config.host, + ); + + if (!deviceConfig.deviceToken || !deviceConfig.privateKey) { + throw new KeeperSdkError( + "Device is not registered for this host. Perform a normal login first to register the device before using session token login.", + ResultCodes.DEVICE_NOT_REGISTERED, + ); + } + + this.auth = await this.createAuth(); + this.sessionManager.setLastUsername(username); + + await this.auth.loginV3({ + username, + givenSessionToken: sessionToken, + loginType: Authentication.LoginType.NORMAL, + loginMethod: Authentication.LoginMethod.EXISTING_ACCOUNT, + }); + + if (!this.auth.sessionToken) { + throw new KeeperSdkError( + "Session token login failed — token may be expired or invalid.", + ResultCodes.SESSION_TOKEN_EXPIRED, + ); + } + + this.synced = false; + this.log.info(`Logged in as ${username} (via session token)`); + } + + /** + * Persist device token and private key for this vault host in session storage so + * {@link loginWithSessionToken} and {@link resumeSession} work without a prior password login on this machine. + * Values use the same base64 / base64url decoding as {@link SessionManager} (`normal64Bytes`). + */ + public async registerDevice( + deviceToken: string, + privateKey: string, + options?: { username?: string }, + ): Promise { + const host = this.config.host; + const save = this.sessionManager.createOnDeviceConfig(host); + await save({ + deviceToken: normal64Bytes(deviceToken), + privateKey: normal64Bytes(privateKey), + }); + if (options?.username) { + this.sessionManager.setLastUsername(options.username); + } + this.log.info(`Device credentials stored for host ${host}`); + } + + public getSessionToken(): string | undefined { + return this.auth?.sessionToken || undefined; + } + + /** + * Resume a session from extension-exported {@link SessionRestoreInput}. + * Verifies the token with a lightweight server call so an expired session + * fails here, not later from `sync()`. + */ + public async restoreSession(input: SessionRestoreInput): Promise { + const params = toSessionParams(input); + await this.sessionManager.saveSessionParameters(params); + this.sessionManager.setLastUsername(params.username); + + this.auth = await this.createAuth(); + await this.auth.continueSession(); + + if (!this.auth.sessionToken) { + throw new KeeperSdkError( + "Session restore failed — session token may be expired or invalid.", + ResultCodes.SESSION_TOKEN_EXPIRED, + ); + } + + try { + await this.auth.loadAccountSummary(); + } catch (err) { + const code = extractResultCode(err); + const msg = extractErrorMessage(err); + this.disconnect(); + const isExpired = code === ResultCodes.SESSION_TOKEN_EXPIRED; + throw new KeeperSdkError( + isExpired + ? `Session token rejected by server (${code}): ${msg}. Re-export session JSON and try again.` + : `Session restore failed: ${msg}`, + code ?? ResultCodes.SESSION_TOKEN_EXPIRED, + ); + } + + this.synced = false; + this.log.info(`Session restored for ${params.username}`); + } + + public async getAccountUsername(): Promise { + return this.sessionManager.getLastUsername(); + } + + public async resumeSession(): Promise { + const username = await this.sessionManager.getLastUsername(); + if (!username) { + throw new KeeperSdkError( + "No previous login found. Perform a normal login first.", + ResultCodes.NO_PREVIOUS_LOGIN, + ); + } + + this.sessionManager.setLastUsername(username); + + const deviceConfig = await this.sessionManager.getDeviceConfig( + this.config.host, + ); + if (!deviceConfig.deviceToken || !deviceConfig.privateKey) { + throw new KeeperSdkError( + "Device is not registered for this host. Perform a normal login first.", + ResultCodes.DEVICE_NOT_REGISTERED, + ); + } + + const cloneCode = await this.sessionManager.getCloneCode( + this.config.host, + username, + ); + if (!cloneCode) { + throw new KeeperSdkError( + "No clone code found. Persistent login not enabled or clone code expired. Perform a normal login.", + ResultCodes.NO_CLONE_CODE, + ); + } + + this.auth = await this.createAuth({ useSessionResumption: true }); + + await this.auth.loginV3({ + loginType: Authentication.LoginType.NORMAL, + resumeSessionOnly: true, + }); + + if (!this.auth.sessionToken) { + throw new KeeperSdkError( + "Persistent login failed — clone code may be expired or persistent login not enabled. Perform a normal login.", + ResultCodes.PERSISTENT_LOGIN_FAILED, + ); + } + + this.synced = false; + this.log.info(`Session resumed for ${username} (persistent login)`); + } + + public async sync(): Promise { + const auth = this.getAuthOrThrow(); + + try { + const result = await syncDown({ + auth, + storage: this.storage, + logFormat: this.config.logFormat, + }); + if (result.error) { + this.log.error("Sync error:", result.error); + throw new KeeperSdkError( + `Sync failed: ${result.error}`, + ResultCodes.SYNC_FAILED, + ); + } + this.synced = true; + return result; + } catch (e) { + this.log.error("Sync failed:", extractErrorMessage(e)); + throw e; + } + } + + public async batch(fn: () => Promise): Promise { + this.batchDepth++; + try { + await fn(); + } finally { + this.batchDepth--; + if (this.batchDepth === 0 && this.config.autoSync) { + await this.sync(); + } + } + } + + private async syncIfNeeded(): Promise { + if (this.batchDepth > 0) { + this.synced = false; + return; + } + if (this.config.autoSync) { + await this.sync(); + } else { + this.synced = false; + } + } + + public getRecords(): DRecord[] { + return this.storage.getRecords(); + } + + public getRecordByUid(uid: string): DRecord | undefined { + return this.storage.getByUid(VaultObjectKind.Record, uid); + } + + public findRecord(uidOrTitle: string): DRecord | undefined { + const byUid = this.getRecordByUid(uidOrTitle); + if (byUid) return byUid; + + const lowerUidOrTitle = uidOrTitle.toLowerCase(); + return this.getRecords().find( + (record) => getRecordTitle(record).toLowerCase() === lowerUidOrTitle, + ); + } + + public findRecords(criteria: string): DRecord[] { + return searchRecords(this.getRecords(), criteria); + } + + public getRecordsByVersion(version: number): DRecord[] { + return this.getRecords().filter((record) => record.version === version); + } + + public getRecordsByType(recordType: string): DRecord[] { + return this.getRecords().filter( + (record) => getRecordType(record) === recordType, + ); + } + + public getRecordMetadata(): DRecordMetadata[] { + return this.storage.getAll(VaultObjectKind.Metadata); + } + + public getRecordMetadataByUid(uid: string): DRecordMetadata | undefined { + return this.storage.getByUid( + VaultObjectKind.Metadata, + uid, + ); + } + + public getSharedFolders(): DSharedFolder[] { + return this.storage.getAll(FolderKind.SharedFolder); + } + + public getTeams(): DTeam[] { + return this.storage.getAll(VaultObjectKind.Team); + } + + public getUserFolders(): DUserFolder[] { + return this.storage.getAll(FolderKind.UserFolder); + } + + public async listFolder( + options?: ListFolderOptions, + ): Promise { + return this.folderManager.listFolder(options ?? {}); + } + + public listSharedFolders( + options?: ListSharedFoldersOptions, + ): ListSharedFolderRow[] { + return this.sharedFolderManager.listSharedFolders(options ?? {}); + } + + public async listTeams(options?: ListTeamsOptions): Promise { + return this.teamManager.listTeams(options ?? {}); + } + + public async listUsers(options?: ListUsersOptions): Promise { + return this.userManager.listUsers(options ?? {}); + } + + public async viewUser(identifier: string): Promise { + return this.userManager.viewUser(identifier); + } + + public async addUsers(input: AddUserInput): Promise { + return this.userManager.addUsers(input); + } + + public formatAddUserResult( + result: AddUserResult, + options: FormatAddUserResultOptions = {}, + ): FormattedAddUserTable { + return this.userManager.formatAddUserResult(result, options); + } + + public async updateUsers(input: UpdateUserInput): Promise { + return this.userManager.updateUsers(input); + } + + public formatUpdateUserResult( + result: UpdateUserResult, + ): FormattedUpdateUserTable { + return this.userManager.formatUpdateUserResult(result); + } + + public async deleteUsers(input: DeleteUserInput): Promise { + return this.userManager.deleteUsers(input); + } + + public formatDeleteUserResult( + result: DeleteUserResult, + ): FormattedDeleteUserTable { + return this.userManager.formatDeleteUserResult(result); + } + + public async actionUsers(input: UserActionInput): Promise { + return this.userManager.actionUsers(input); + } + + public formatUserActionResult( + result: UserActionResult, + ): FormattedUserActionTable { + return this.userManager.formatUserActionResult(result); + } + + public async aliasUser(input: AliasUserInput): Promise { + return this.userManager.aliasUser(input); + } + + public async addUsersToTeams( + input: AddUsersToTeamsInput, + ): Promise { + return this.userManager.addUsersToTeams(input); + } + + public async removeUsersFromTeams( + input: RemoveUsersFromTeamsInput, + ): Promise { + return this.userManager.removeUsersFromTeams(input); + } + + public formatTeamUserResult(result: TeamUserResult): FormattedTeamUserTable { + return this.userManager.formatTeamUserResult(result); + } + + public async viewTeam(identifier: string): Promise { + return this.teamManager.viewTeam(identifier); + } + + public async addTeams(input: AddTeamInput): Promise { + const result = await this.teamManager.addTeams(input); + if (result.created > 0) await this.syncIfNeeded(); + return result; + } + + public async updateTeams(input: UpdateTeamInput): Promise { + const result = await this.teamManager.updateTeams(input); + if (result.updated > 0) await this.syncIfNeeded(); + return result; + } + + public async deleteTeams(input: DeleteTeamInput): Promise { + const result = await this.teamManager.deleteTeams(input); + if (result.deleted > 0) await this.syncIfNeeded(); + return result; + } + + public async listRoles(options?: ListRolesOptions): Promise { + return this.roleManager.listRoles(options ?? {}); + } + + public async viewRole(identifier: string): Promise { + return this.roleManager.viewRole(identifier); + } + + public async addRoles(input: AddRoleInput): Promise { + const result = await this.roleManager.addRoles(input); + if (result.created > 0) await this.syncIfNeeded(); + return result; + } + + public async updateRoles(input: UpdateRoleInput): Promise { + const result = await this.roleManager.updateRoles(input); + if (result.updated > 0) await this.syncIfNeeded(); + return result; + } + + public async deleteRoles(input: DeleteRoleInput): Promise { + const result = await this.roleManager.deleteRoles(input); + if (result.deleted > 0) await this.syncIfNeeded(); + return result; + } + + public async changeDirectory(path: string): Promise { + return this.folderManager.changeDirectory(path); + } + + public getCurrentFolderUid(): string | null { + return this.folderManager.getCurrentFolderUid(); + } + + public getWorkingFolderDisplayName(): string { + return this.folderManager.getWorkingFolderDisplayName(); + } + + public async getFolder( + uidOrName: string, + options?: GetFolderOptions, + ): Promise { + return this.folderManager.getFolder(uidOrName, options ?? {}); + } + + public async addFolder(input: AddFolderInput): Promise { + const result = await this.folderManager.addFolder(input); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async mkdir( + path: string, + options?: MkdirOptions, + ): Promise<{ folderUid: string; success: boolean; message?: string }> { + const result = await this.folderManager.mkdir(path, options ?? {}); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async updateFolder( + input: UpdateFolderInput, + ): Promise { + const result = await this.folderManager.updateFolder(input); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async renameFolder( + folderPath: string, + newName: string, + ): Promise { + const result = await this.folderManager.renameFolder(folderPath, newName); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async updateSharedFolderPermissions( + sharedFolderUid: string, + permissions: SharedFolderPermissionsInput, + ): Promise { + const result = await this.folderManager.updateSharedFolderPermissions( + sharedFolderUid, + permissions, + ); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async deleteFolder( + folderRefs: string[], + confirm?: (summary: string) => boolean | Promise, + ): Promise { + const result = await this.folderManager.deleteFolder(folderRefs, confirm); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async rmdir( + patterns: string[], + options?: RmdirOptions, + ): Promise { + const result = await this.folderManager.rmdir(patterns, options ?? {}); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async tree(options?: FolderTreeBuildOptions): Promise { + this.getAuthOrThrow(); + return this.folderManager.folderTreeAscii(options ?? {}); + } + + public getSummary(): VaultSummary { + return { + recordCount: this.storage.getCount(VaultObjectKind.Record), + sharedFolderCount: this.storage.getCount(FolderKind.SharedFolder), + teamCount: this.storage.getCount(VaultObjectKind.Team), + folderCount: this.storage.getCount(FolderKind.UserFolder), + }; + } + + public printRecords(showDetails = false): void { + const records = this.getRecords(); + if (records.length === 0) { + this.log.info("No records found in vault."); + return; + } + this.log.info(`\n=== Vault Records (${records.length}) ===\n`); + for (const record of records) { + this.log.info(formatRecord(record, showDetails)); + } + } + + public async addRecord(input: NewRecordInput): Promise { + const auth = this.getAuthOrThrow(); + const result = await addRecordOp(auth, input); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async updateRecord( + recordUid: string, + data: TypedRecordData, + ): Promise { + const auth = this.getAuthOrThrow(); + + const record = this.getRecordByUid(recordUid); + if (!record) { + return { recordUid, success: false, status: VaultStatus.RecordNotFound }; + } + + const keyBytes = await this.storage.getKeyBytes(recordUid); + if (!keyBytes) { + return { + recordUid, + success: false, + status: VaultStatus.RecordKeyNotFound, + }; + } + + const result = await updateRecordOp( + auth, + recordUid, + data, + record.revision, + keyBytes, + ); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async deleteRecord(recordUid: string): Promise { + const auth = this.getAuthOrThrow(); + const result = await deleteRecordOp(auth, recordUid); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async moveRecord(input: MoveRecordInput): Promise { + const auth = this.getAuthOrThrow(); + const result = await moveRecordOp(auth, this.storage, input); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async shareRecord( + input: ShareRecordInput, + ): Promise { + const auth = this.getAuthOrThrow(); + + const record = + this.getRecordByUid(input.recordUid) || this.findRecord(input.recordUid); + if (!record) { + return { + recordUid: input.recordUid, + email: input.email, + success: false, + status: VaultStatus.RecordNotFound, + message: `Record "${input.recordUid}" not found`, + }; + } + + const keyBytes = await this.storage.getKeyBytes(record.uid); + if (!keyBytes) { + return { + recordUid: record.uid, + email: input.email, + success: false, + status: VaultStatus.RecordKeyNotFound, + message: "Record key not available", + }; + } + + const result = await shareRecordOp(auth, keyBytes, { + ...input, + recordUid: record.uid, + }); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async removeRecordShare( + input: RemoveShareInput, + ): Promise { + const auth = this.getAuthOrThrow(); + const result = await removeRecordShareOp(auth, input); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async getRecordShareInfo( + recordUid: string, + ): Promise { + const auth = this.getAuthOrThrow(); + return getRecordShareInfoOp(auth, recordUid); + } + + public async shareFolder( + input: ShareFolderInput, + ): Promise { + const result = await this.sharedFolderManager.shareFolder(input); + if (result.success) await this.syncIfNeeded(); + return result; + } + + public async getRecordHistory( + recordUid: string, + ): Promise { + const auth = this.getAuthOrThrow(); + + const keyBytes = await this.storage.getKeyBytes(recordUid); + if (!keyBytes) { + return { recordUid, history: [] }; + } + + return getRecordHistoryOp(auth, recordUid, keyBytes); + } + + public getStorage(): VaultStorage { + return this.storage; + } + + public getAuth(): Auth { + return this.getAuthOrThrow(); + } + + public disconnect(): void { + if (this.auth) { + try { + this.auth.disconnect(); + } catch (err) { + this.log.debug("disconnect error:", extractErrorMessage(err)); + } + this.auth = null; + } + this.synced = false; + this.folderSession.currentFolderUid = null; + } + + public async logout(): Promise { + if (this.auth) { + try { + await this.auth.logout(); + } catch (err) { + this.log.debug("logout error:", extractErrorMessage(err)); + } + } + this.disconnect(); + this.log.info("Logged out."); + } + + public get host(): string { + return this.config.host; + } + + public get isLoggedIn(): boolean { + return this.auth !== null && !!this.auth.sessionToken; + } + + public get isSynced(): boolean { + return this.synced; + } } diff --git a/KeeperSdk/tsconfig.json b/KeeperSdk/tsconfig.json index 15eafc2b..04586731 100644 --- a/KeeperSdk/tsconfig.json +++ b/KeeperSdk/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es2018", - "sourceMap": true, - "strict": false, - "skipLibCheck": true, - "esModuleInterop": true, - "declaration": true, - "rootDir": "src", - "outDir": "dist", - "types": ["node"] - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "module": "commonjs", + "target": "es2018", + "sourceMap": true, + "strict": false, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true, + "rootDir": "src", + "outDir": "dist", + "types": ["node"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] } From 283b96fcb4708b180df2e3d33a699ed8219ccf5d Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 15 Jun 2026 10:51:37 +0530 Subject: [PATCH 23/23] fixed formatting for keeper api --- keeperapi/src/restMessages.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index 46d2ad18..c6106c33 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -561,22 +561,12 @@ export const getEnterprisePublicKeyMessage = (): RestOutMessage => - createMessage( - data, - 'vault/get_share_objects', - Records.GetShareObjectsRequest, - Records.GetShareObjectsResponse - ) + createMessage(data, 'vault/get_share_objects', Records.GetShareObjectsRequest, Records.GetShareObjectsResponse) export const getTeamMembersMessage = ( data: Enterprise.IGetTeamMemberRequest ): RestMessage => - createMessage( - data, - 'vault/get_team_members', - Enterprise.GetTeamMemberRequest, - Enterprise.GetTeamMemberResponse - ) + createMessage(data, 'vault/get_team_members', Enterprise.GetTeamMemberRequest, Enterprise.GetTeamMemberResponse) export const getEnterpriseDataForUserMessage = ( data: Enterprise.IEnterpriseDataRequest