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