diff --git a/KeeperSdk/package.json b/KeeperSdk/package.json index 377dd140..c0601ff2 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -9,6 +9,7 @@ }, "license": "ISC", "main": "dist/index.js", + "browser": "dist/browser.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", diff --git a/KeeperSdk/src/api.ts b/KeeperSdk/src/api.ts new file mode 100644 index 00000000..95dc08c6 --- /dev/null +++ b/KeeperSdk/src/api.ts @@ -0,0 +1,407 @@ +export { SessionManager } from './auth/SessionManager' +export type { + KeeperJsonConfig, + ConfigLoader, + ConfigurationUser, + ConfigurationServerConfig, + ConfigurationDeviceConfig, +} from './auth/SessionManager' +export { connectSdkPlatform, getSdkPlatform, isSdkPlatformConnected } from './platform' +export type { SdkPlatform, SdkReadline, SdkRuntime } from './platform' + +export { InMemoryStorage } from './storage/InMemoryStorage' + +export { + Logger, + ConsoleLogger, + LogLevel, + logger, + setLogger, + getLogger, + resetLogger, + KeeperSdkError, + isKeeperError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + AuthDefaults, + ResultCodes, + KEEPER_PUBLIC_HOSTS, + isBoolean, + isString, + isNonEmptyString, + isNumber, + isObject, + anyIsBoolean, + EMAIL_PATTERN, + EMAIL_LIST_SEPARATOR_PATTERN, + TOKEN_SEPARATOR_PATTERN, + REGEX_ESCAPE_PATTERN, + TRAILING_EQUALS_PATTERN, + WHITESPACE_PATTERN, + isValidEmail, + escapeRegExp, + DEFAULT_PASSWORD_LENGTH, + PW_SPECIAL_CHARACTERS, + GEN_PASSWORD_ALGORITHMS, + KeeperPasswordGenerator, + generatePasswordFromOptions, + resolveGenPasswordAlgorithm, + generatePassword, + parseGenParametersFromValue, + isGenerateFieldValue, + parseGeneratePasswordFlag, +} from './utils' +export type { + ILogger, + Nullable, + Optional, + DeepPartial, + Immutable, + GenPasswordAlgorithm, + PasswordGenerationOptions, + PasswordComplexityPolicy, + PassphraseGenOptions, +} from './utils' + +export { + searchRecords, + formatRecord, + formatRecordFields, + getRecordTitle, + getRecordType, + getRecordFields, + getRecordSummary, + getRecordDescription, + getRecordCategory, + getRecordPassword, + getRecordLogin, + getRecordUrl, + getRecordTotpUrl, + RecordVersion, +} from './records/RecordUtils' +export type { RecordSummary } from './records/RecordUtils' +export { formatRecordsListTable, renderRecordsListAsciiTable, renderRecordsListTable } from './records/listRecordsTable' +export type { FormattedRecordsListTable } from './records/listRecordsTable' +export { parseTotpUrl, getTotpCode } from './records/Totp' +export type { TotpAlgorithm, TotpParams, TotpCode } from './records/Totp' +export { addRecord, updateRecord, deleteRecord, getRecordHistory, moveRecord } from './records/RecordOperations' +export type { + PasswordRecordData, + TypedRecordData, + RecordFieldInput, + NewRecordInput, + AddRecordResult, + UpdateRecordResult, + DeleteRecordResult, + HistoryEntry, + RecordHistoryResult, + MoveRecordInput, + MoveRecordResult, +} from './records/RecordOperations' + +export { + shareRecord, + removeRecordShare, + getRecordShareInfo, + loadUserShareKeys, + encryptKeyForRecipient, + sendShareInviteIfNeeded, + loadUserShareKeysOrInvite, + parseRecordSharingStatus, + buildNsfRecordSharePermission, + buildNsfRecordRevokePermission, +} from './sharing/Sharing' +export type { + ShareRecordInput, + ShareRecordResult, + RemoveShareInput, + RemoveShareResult, + RecordShareInfo, + RecordUserPermission, + RecordSharedFolderPermission, + UserShareKeys, +} from './sharing/Sharing' + +export { KeeperVault } from './vault/KeeperVault' +export type { KeeperVaultConfig, VaultSummary } from './vault/KeeperVault' + +export { buildWhoamiInfo, normalizeServerHost, resolveDataCenter } from './account/whoamiInfo' +export type { WhoamiInfo, BuildWhoamiInfoInput } from './account/whoamiInfo' + +export type { SessionRestoreInput } from './auth/sessionRestore' +export { + toSessionParams, + validateSessionRestoreInput, + sessionRestoreFromJson, + resolveSessionRestorePayload, +} from './auth/sessionRestore' + +export { getFolder, findFolder, GetFolderFormat } from './folders/getFolder' +export type { + GetFolderOptions, + GetFolderResult, + GetFolderResultFolder, + GetFolderResultSharedFolder, + GetFolderFormatInput, + FoundFolder, +} from './folders/getFolder' + +export { + FolderKind, + ParentFolderKind, + FolderObjectType, + FolderResultStatus, + DeleteResolution, + DeleteObjectType, + folderKindFromString, +} from './folders/folderHelpers' +export type { FolderKindOrLiteral, ClassicFolderKind } from './folders/folderHelpers' + +export { listFolder, findFolderUidByNameOrUid, listRootUserFolders } from './folders/listFolder' +export type { + ListFolderOptions, + ListFolderResult, + ListFolderFolderSimple, + ListFolderRecordSimple, + ListFolderFolderDetail, + ListFolderRecordDetail, +} from './folders/listFolder' + +export { + listSharedFolders, + formatSharedFoldersTable, + renderSharedFoldersAsciiTable, +} from './sharedFolders/listSharedFolders' +export type { + ListSharedFoldersOptions, + ListSharedFolderRow, + FormattedSharedFoldersTable, +} from './sharedFolders/listSharedFolders' + +export { shareFolder, ShareFolderAction, ShareFolderUserResultStatus } from './sharedFolders/shareFolder' +export type { + ShareFolderActionInput, + ShareFolderInput, + ShareFolderResult, + ShareFolderUserStatus, +} from './sharedFolders/shareFolder' + +export { + changeDirectory, + createVaultFolderSession, + tryResolvePath, + resolveSingleFolder, + getWorkingFolderDisplayName, + findParentFolderUid, + splitPathComponents, +} from './folders/changeDirectory' +export type { VaultFolderSession, ChangeDirectoryResult, TryResolvePathResult } from './folders/changeDirectory' + +export { addFolder, mkdir } from './folders/addFolder' +export type { AddFolderInput, AddFolderResult, MkdirOptions } from './folders/addFolder' + +export { updateFolder, renameFolder, updateSharedFolderPermissions } from './folders/updateFolder' +export type { UpdateFolderInput, UpdateFolderResult, RenameFolderResult } from './folders/updateFolder' + +export { deleteFolder, rmdir, resolveRmdirPatternsToFolderUids, buildFolderDeleteObject } from './folders/deleteFolder' +export type { DeleteFolderResult, RmdirOptions } from './folders/deleteFolder' + +export { + buildFolderTree, + renderFolderTreeAscii, + folderTreeAscii, + userPermissionToText, + recordPermissionToText, +} from './folders/folderTree' +export type { FolderTreeBuildOptions, FolderTreeNode, FolderTreeResult } from './folders/folderTree' + +export { FolderManager } from './folders/FolderManager' +export type { AuthProvider, SharedFolderPermissionsInput } from './folders/FolderManager' + +export { SharedFolderManager } from './sharedFolders/SharedFolderManager' + +export { + listTeams, + formatTeamsTable, + renderTeamsAsciiTable, + formatTeamRestricts, + TeamColumn, + SUPPORTED_TEAM_COLUMNS, + DEFAULT_TEAM_COLUMNS, +} from './teams/listTeams' +export type { + ListTeamsOptions, + ListTeamRow, + TeamColumnInput, + FormattedTeamsTable, + FormatTeamsTableOptions, +} from './teams/listTeams' + +export { viewTeam, formatTeamView, teamViewTable } from './teams/viewTeam' +export type { + TeamView, + TeamRoleInfo, + TeamUserInfo, + FormatTeamViewOptions, + FormattedTeamViewTable, + TeamViewTableRow, +} from './teams/viewTeam' + +export { changeTeamRoles, TeamRoleStatus } from './teams/teamRole' +export type { ChangeTeamRolesInput, TeamRoleResult, TeamRoleItemResult } from './teams/teamRole' + +export { + addUsersToTeams, + removeUsersFromTeams, + formatTeamUserResult, + renderTeamUserAsciiTable, + TeamUserStatus, + TeamUserSkipReason, +} from './users/teamUser' +export type { + AddUsersToTeamsInput, + RemoveUsersFromTeamsInput, + TeamUserItemResult, + TeamUserResult, + FormattedTeamUserTable, +} from './users/userTypes' + +export { formatAddUserResult, renderAddUserAsciiTable, AddUserStatus } from './users/addUser' +export { formatUpdateUserResult, renderUpdateUserAsciiTable, UpdateUserStatus } from './users/updateUser' +export { formatDeleteUserResult, renderDeleteUserAsciiTable, DeleteUserStatus } from './users/deleteUser' +export { + formatUserActionResult, + renderUserActionAsciiTable, + UserAction, + UserActionStatus, + UserActionSkipReason, +} from './users/actionUser' + +export { + formatAddTeamResult, + renderAddTeamAsciiTable, + AddTeamStatus, + AddTeamSkipReason, + AddTeamSourceKind, +} from './teams/addTeam' +export { formatUpdateTeamResult, renderUpdateTeamAsciiTable, UpdateTeamStatus } from './teams/updateTeam' +export { formatDeleteTeamResult, renderDeleteTeamAsciiTable, DeleteTeamStatus } from './teams/deleteTeam' + +export { + listUsers, + formatUsersTable, + renderUsersAsciiTable, + UserColumn, + SUPPORTED_USER_COLUMNS, + DEFAULT_USER_COLUMNS, +} from './users/listUsers' +export type { + UserColumnInput, + ListUsersOptions, + ListUserRow, + FormattedUsersTable, + FormatUsersTableOptions, +} from './users/listUsers' + +export { viewUser, formatUserView, userViewTable } from './users/viewUser' +export type { UserView, FormatUserViewOptions, FormattedUserViewTable, UserViewTableRow } from './users/userTypes' + +export { + listRoles, + formatRolesTable, + renderRolesAsciiTable, + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + DEFAULT_ROLE_COLUMNS, + ALL_COLUMNS_WILDCARD, + viewRole, + formatRoleView, + roleViewTable, + formatAddRoleResult, + renderAddRoleAsciiTable, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + formatCopyRoleResult, + renderCopyRoleAsciiTable, + formatRoleUserResult, + renderRoleUserAsciiTable, + formatManageRoleNodesResult, + renderManageRoleNodesAsciiTable, + formatChangeRolePrivilegesResult, + renderChangeRolePrivilegesAsciiTable, +} from './roles' +export type { + ListRolesOptions, + ListRoleRow, + RoleColumnInput, + FormattedRolesTable, + FormatRolesTableOptions, + RoleView, + RoleTeamInfo, + RoleUserInfo, + RoleManagedNodeInfo, + RoleEnforcementInfo, + FormatRoleViewOptions, + FormattedRoleViewTable, + RoleViewTableRow, +} from './roles' + +export { + listNodes, + formatNodesTable, + renderNodesAsciiTable, + NodeColumn, + SUPPORTED_NODE_COLUMNS, + DEFAULT_NODE_COLUMNS, + viewNode, + formatNodeView, + nodeViewTable, + formatAddNodeResult, + renderAddNodeAsciiTable, + formatUpdateNodeResult, + renderUpdateNodeAsciiTable, + formatDeleteNodeResult, + renderDeleteNodeAsciiTable, +} from './nodes' +export type { + ListNodesOptions, + ListNodeRow, + NodeColumnInput, + FormattedNodesTable, + FormatNodesTableOptions, + NodeView, + FormatNodeViewOptions, + FormattedNodeViewTable, + NodeViewTableRow, + AddNodeInput, + AddNodeResult, + FormattedAddNodeTable, + UpdateNodeInput, + UpdateNodeResult, + FormattedUpdateNodeTable, + DeleteNodeInput, + DeleteNodeResult, + FormattedDeleteNodeTable, +} from './nodes' + +export { Auth, KeeperEnvironment, syncDown, Authentication } from '@keeper-security/keeperapi' + +export type { + DRecord, + DRecordMetadata, + DSharedFolder, + DTeam, + DUserFolder, + VaultStorage, + SyncResult, + SyncDownOptions, + ClientConfiguration, + DeviceConfig, + SessionStorage, + AuthUI3, + KeeperError, + LoginError, +} from '@keeper-security/keeperapi' diff --git a/KeeperSdk/src/browser.ts b/KeeperSdk/src/browser.ts new file mode 100644 index 00000000..01834916 --- /dev/null +++ b/KeeperSdk/src/browser.ts @@ -0,0 +1,8 @@ +import { connectSdkPlatform } from './platform' +import { browserSdkPlatform } from './platform/browser/platform' + +connectSdkPlatform(browserSdkPlatform) + +export * from './api' +/** Nested Share Folder / Keeper Drive APIs (also on Node `index.ts`). */ +export * from './nestedShareFolders' diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 12799180..78064728 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -3,6 +3,8 @@ import { nodeSdkPlatform } from './platform/node/platform' connectSdkPlatform(nodeSdkPlatform) +export * from './api' + export { ConsoleAuthUI } from './auth/ConsoleAuthUI' export { UnavailableAuthUI } from './auth/UnavailableAuthUI' export { FileConfigLoader } from './auth/node/FileConfigLoader' @@ -70,8 +72,28 @@ export { isValidEmail, escapeRegExp, resolveSearchPattern, + DEFAULT_PASSWORD_LENGTH, + PW_SPECIAL_CHARACTERS, + GEN_PASSWORD_ALGORITHMS, + KeeperPasswordGenerator, + generatePasswordFromOptions, + resolveGenPasswordAlgorithm, + generatePassword, + parseGenParametersFromValue, + isGenerateFieldValue, + parseGeneratePasswordFlag, +} from './utils' +export type { + ILogger, + Nullable, + Optional, + DeepPartial, + Immutable, + GenPasswordAlgorithm, + PasswordGenerationOptions, + PasswordComplexityPolicy, + PassphraseGenOptions, } from './utils' -export type { ILogger, Nullable, Optional, DeepPartial, Immutable } from './utils' export { searchRecords, @@ -230,6 +252,55 @@ export { userPermissionToText, recordPermissionToText, } from './folders/folderTree' + +export { + runAuditReport, + runActionReport, + runPasswordReport, + getPasswordStrength, + calculatePasswordScore, + parsePasswordPolicy, + isPasswordCompliant, + buildPasswordPolicySummary, + getAllowedActions, + getDefaultDaysSince, + AuditReportManager, + ActionReportManager, + EnterpriseReportManager, + AuditReportOrder, + AuditReportFormat, + AuditOutputFormat, + AuditAggregate, + SUMMARY_REPORT_TYPES, + CREATED_PRESETS, + TargetUserStatus, + AdminAction, + ActionReportColumn, + DEFAULT_ACTION_REPORT_COLUMNS, + SUPPORTED_ACTION_REPORT_COLUMNS, + DEFAULT_TRUNCATION_LENGTH, + SUPPORTED_RECORD_VERSIONS, +} from './enterpriseReport' +export type { + AuditReportOptions, + AuditReportResult, + AuditReportFilter, + CreatedFilterCriteria, + CreatedPreset, + AuditEventOverviewReportRow, + AuditReportType, + AuditSummaryReportType, + ActionReportEntry, + ActionReportOptions, + ActionReportResult, + ActionResult, + PasswordPolicy, + PasswordStrength, + PasswordReportRow, + PasswordReportOptions, + PasswordReportResult, +} from './enterpriseReport' + export type { FolderTreeBuildOptions, FolderTreeNode, FolderTreeResult } from './folders/folderTree' export { FolderManager } from './folders/FolderManager' @@ -302,6 +373,27 @@ export { formatDeleteRoleResult, renderDeleteRoleAsciiTable, DeleteRoleStatus, + setRoleEnforcements, + formatSetRoleEnforcementsResult, + renderRoleEnforcementAsciiTable, + RoleEnforcementStatus, + copyRoles, + formatCopyRoleResult, + renderCopyRoleAsciiTable, + addUsersToRoles, + removeUsersFromRoles, + formatRoleUserResult, + renderRoleUserAsciiTable, + RoleUserStatus, + RoleUserSkipReason, + manageRoleNodes, + formatManageRoleNodesResult, + renderManageRoleNodesAsciiTable, + RoleManagedNodeStatus, + changeRolePrivileges, + formatChangeRolePrivilegesResult, + renderChangeRolePrivilegesAsciiTable, + RolePrivilegeStatus, } from './roles' export type { ListRolesOptions, @@ -335,56 +427,77 @@ export type { RoleToggleInput, RoleToggle, EnforcementPair, + SetRoleEnforcementsInput, + RoleEnforcementItemResult, + SetRoleEnforcementsResult, + FormattedRoleEnforcementTable, + CopyRoleInput, + CopyRoleResult, + FormattedCopyRoleTable, + AddUsersToRolesInput, + RemoveUsersFromRolesInput, + RoleUserItemResult, + RoleUserResult, + FormattedRoleUserTable, + ManageRoleNodesAction, + ManageRoleNodesInput, + RoleManagedNodeItemResult, + ManageRoleNodesResult, + FormattedManageRoleNodesTable, + ChangeRolePrivilegesInput, + RolePrivilegeItemResult, + ChangeRolePrivilegesResult, + FormattedRolePrivilegeTable, } from './roles' export { - runAuditReport, - runActionReport, - runPasswordReport, - getPasswordStrength, - calculatePasswordScore, - parsePasswordPolicy, - isPasswordCompliant, - buildPasswordPolicySummary, - getAllowedActions, - getDefaultDaysSince, - AuditReportManager, - ActionReportManager, - EnterpriseReportManager, - AuditReportOrder, - AuditReportFormat, - AuditOutputFormat, - AuditAggregate, - SUMMARY_REPORT_TYPES, - CREATED_PRESETS, - TargetUserStatus, - AdminAction, - ActionReportColumn, - DEFAULT_ACTION_REPORT_COLUMNS, - SUPPORTED_ACTION_REPORT_COLUMNS, - PW_SPECIAL_CHARACTERS, - DEFAULT_TRUNCATION_LENGTH, - SUPPORTED_RECORD_VERSIONS, -} from './enterpriseReport' + NodeManager, + listNodes, + formatNodesTable, + renderNodesAsciiTable, + NodeColumn, + SUPPORTED_NODE_COLUMNS, + DEFAULT_NODE_COLUMNS, + viewNode, + formatNodeView, + nodeViewTable, + addNodes, + formatAddNodeResult, + renderAddNodeAsciiTable, + AddNodeStatus, + AddNodeSkipReason, + updateNodes, + formatUpdateNodeResult, + renderUpdateNodeAsciiTable, + UpdateNodeStatus, + deleteNodes, + formatDeleteNodeResult, + renderDeleteNodeAsciiTable, + DeleteNodeStatus, +} from './nodes' export type { - AuditReportOptions, - AuditReportResult, - AuditReportFilter, - CreatedFilterCriteria, - CreatedPreset, - AuditEventOverviewReportRow, - AuditReportType, - AuditSummaryReportType, - ActionReportEntry, - ActionReportOptions, - ActionReportResult, - ActionResult, - PasswordPolicy, - PasswordStrength, - PasswordReportRow, - PasswordReportOptions, - PasswordReportResult, -} from './enterpriseReport' + ListNodesOptions, + ListNodeRow, + NodeColumnInput, + FormattedNodesTable, + FormatNodesTableOptions, + NodeView, + FormatNodeViewOptions, + FormattedNodeViewTable, + NodeViewTableRow, + AddNodeInput, + AddNodeResult, + AddNodeItemResult, + FormattedAddNodeTable, + UpdateNodeInput, + UpdateNodeResult, + UpdateNodeItemResult, + FormattedUpdateNodeTable, + DeleteNodeInput, + DeleteNodeResult, + DeleteNodeItemResult, + FormattedDeleteNodeTable, +} from './nodes' export { viewTeam, formatTeamView, teamViewTable } from './teams/viewTeam' export type { @@ -435,6 +548,9 @@ export type { FormattedDeleteTeamRow, } from './teams/deleteTeam' +export { changeTeamRoles, TeamRoleStatus } from './teams/teamRole' +export type { ChangeTeamRolesInput, TeamRoleResult, TeamRoleItemResult } from './teams/teamRole' + export { TeamManager } from './teams/TeamManager' export { @@ -521,6 +637,20 @@ export { TeamUserSkipReason, } from './users/teamUser' +export { + updateUsersOnTeams, + formatUpdateTeamUserResult, + renderUpdateTeamUserAsciiTable, + UpdateTeamUserStatus, + UpdateTeamUserSkipReason, +} from './users/updateTeamUser' +export type { + UpdateUsersOnTeamsInput, + UpdateTeamUserItemResult, + UpdateUsersOnTeamsResult, + FormattedUpdateTeamUserTable, +} from './users/updateTeamUser' + export { UserManager } from './users/UserManager' export { @@ -558,15 +688,18 @@ export { formatNsfDetail, formatNsfJson, formatNsfRecordJson, + formatNsfFolderJson, toNsfRecordJsonView, + toNsfFolderJsonView, + NSF_UNMASK_WARNING, linkNestedShareRecord, NsfRemoveOperation, + NsfRemoveFolderOperation, removeNestedShareRecords, formatRemoveNsfPreview, collectRemoveNsfWarnings, mkdirNestedShareFolder, NSF_FOLDER_COLORS, - NsfRemoveFolderOperation, removeNestedShareFolders, formatRemoveNsfFolderPreview, GetNsfRecordDetailsFormat, @@ -583,7 +716,15 @@ export { resolveNsfFieldValue, clearNsfRecordTypeCache, checkRecordEditPermission, + checkFolderEditPermission, + checkFolderSharePermission, + checkRecordSharePermission, + ROOT_FOLDER_UID, + NSF_RECORD_PERMISSION_ROLES, + NSF_MAX_FOLDER_UPDATES, + getFolderPermissionsForRole, updateNestedShareFolder, + updateNestedShareFolders, shareNestedShareFolder, shareNestedShareRecord, formatNsfRecordSharePlan, @@ -598,22 +739,21 @@ export { resolveNsfShareRecipient, MIN_SHARE_EXPIRATION_MS, TeamGetKeysResponseKeyType, - getNsfRecordShortcuts, listNsfShortcuts, - keepNsfShortcut, + getNsfRecordShortcuts, formatNsfShortcutOutput, + keepNsfShortcut, formatKeepNsfShortcutPlan, transferNestedShareRecords, formatTransferNestedShareRecordResults, - NSF_RECORD_PERMISSION_ROLES, NSFShareRoleName, NsfShareCommandName, NSF_SHARE_EXPIRATION_NEVER, + NsfRecordPermissionAction, NsfFolderShareAction, NsfFolderShareActionTaken, NsfRecordShareAction, NsfRecordShareActionTaken, - NsfRecordPermissionAction, NsfResultStatus, NsfTransferApiStatus, NsfPermissionFailureCode, @@ -626,7 +766,6 @@ export { resolveNsfRoleName, getNsfAccessRoleLabel, normalizeNsfRecordPermissionRole, - getFolderPermissionsForRole, NestedShareFolderManager, } from './nestedShareFolders' export type { @@ -679,16 +818,20 @@ export type { RecordFieldEntry, UpdateNsfFolderInput, UpdateNsfFolderResult, - NsfFolderShareActionInput, + UpdateNsfFolderBatchItem, + UpdateNsfFolderBatchResultItem, + UpdateNsfFoldersResult, ShareNestedShareFolderInput, ShareNestedShareFolderResult, - NsfFolderShareResultItem, - NsfRecordShareActionInput, ShareNestedShareRecordInput, ShareNestedShareRecordResult, + NsfFolderShareActionInput, + NsfFolderShareResultItem, + NsfRecordShareActionInput, NsfRecordSharePlanItem, NsfRecordShareResultItem, NsfShortcutRow, + NsfShortcutFolder, ListNsfShortcutsOptions, KeepNsfShortcutInput, KeepNsfShortcutPlanItem, @@ -697,13 +840,14 @@ export type { TransferNestedShareRecordInput, TransferNestedShareRecordResult, TransferNestedShareRecordResultItem, - NsfRecordPermissionRole, NsfRecordPermissionActionInput, UpdateNsfRecordPermissionInput, UpdateNsfRecordPermissionResult, NsfRecordPermissionPlan, NsfRecordPermissionPlanItem, NsfRecordPermissionFailure, + NsfRecordPermissionRole, + NsfRecordPermissionRoleInput, ParseShareExpirationInput, NsfTeamPublicKeys, NsfResolvedShareRecipient, diff --git a/KeeperSdk/src/nestedShareFolders/index.ts b/KeeperSdk/src/nestedShareFolders/index.ts index 8c72ee47..33de2dc6 100644 --- a/KeeperSdk/src/nestedShareFolders/index.ts +++ b/KeeperSdk/src/nestedShareFolders/index.ts @@ -1,4 +1,5 @@ export { + ROOT_FOLDER_UID, KeeperDriveKind, NsfItemType, formatAccessRoleType, diff --git a/KeeperSdk/src/nestedShareFolders/nsfConstants.ts b/KeeperSdk/src/nestedShareFolders/nsfConstants.ts index f0c48780..5ac1b11d 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfConstants.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfConstants.ts @@ -1,6 +1,8 @@ import { Folder } from '@keeper-security/keeperapi' import { KeeperSdkError, ResultCodes } from '../utils' +export const ROOT_FOLDER_UID = 'AAAAAAAAAAAAAAAAAPmtNA' + export const NSF_LEGACY_RECORD_MSG = "Record '{0}' is a legacy vault record. Nested Share Folder commands operate only on Nested Share Records." diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts index 54d0c8d1..2010a42e 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -43,6 +43,8 @@ import { } from './nsfConstants' import { NsfAccessRoleLabel, NSF_ACCESS_ROLE_LABELS, type ParseShareExpirationInput } from './nsfTypes' +export { ROOT_FOLDER_UID } from './nsfConstants' + export enum KeeperDriveKind { Folder = 'keeper_drive_folder', FolderAccess = 'keeper_drive_folder_access', diff --git a/KeeperSdk/src/teams/TeamManager.ts b/KeeperSdk/src/teams/TeamManager.ts index d846bb05..372c28b7 100644 --- a/KeeperSdk/src/teams/TeamManager.ts +++ b/KeeperSdk/src/teams/TeamManager.ts @@ -42,6 +42,7 @@ import { type DeleteTeamResult, type FormattedDeleteTeamTable, } from './deleteTeam' +import { changeTeamRoles, type ChangeTeamRolesInput, type TeamRoleResult } from './teamRole' export type AuthProvider = () => Auth @@ -112,6 +113,10 @@ export class TeamManager { return renderDeleteTeamAsciiTable(table) } + public async changeTeamRoles(input: ChangeTeamRolesInput): Promise { + return changeTeamRoles(this.requireAuth(), input) + } + private requireAuth(): Auth { const auth = this.authProvider() if (!auth) { diff --git a/KeeperSdk/src/teams/addTeam.ts b/KeeperSdk/src/teams/addTeam.ts index 5e66f661..82e19b3e 100644 --- a/KeeperSdk/src/teams/addTeam.ts +++ b/KeeperSdk/src/teams/addTeam.ts @@ -49,6 +49,8 @@ export enum AddTeamStatus { export enum AddTeamSkipReason { AlreadyExistsInParent = 'already_exists_in_parent', ExistsElsewhereDeclined = 'exists_elsewhere_declined', + NotQueued = 'not_queued', + AlreadyActive = 'already_active', } export enum AddTeamSourceKind { @@ -76,6 +78,8 @@ export type AddTeamInput = { restrictView?: TeamRestrictionInput force?: boolean confirm?: AddTeamConfirm + /** When true, only promote queued teams (SCIM/Bridge); do not create new teams. */ + approveQueued?: boolean } export type AddTeamItemResult = { @@ -121,6 +125,7 @@ export async function addTeams(auth: Auth, input: AddTeamInput): Promise 0) { + items.push({ + teamUid: conflicts[0]!.team_uid, + teamName: conflicts[0]!.name || raw, + nodeId: conflicts[0]!.node_id, + source: AddTeamSourceKind.NewName, + status: AddTeamStatus.Skipped, + skipReason: AddTeamSkipReason.AlreadyActive, + message: `Team "${conflicts[0]!.name || raw}" already exists (not queued).`, + }) + } else { + items.push({ + teamUid: '', + teamName: raw, + nodeId: parentNodeId, + source: AddTeamSourceKind.NewName, + status: AddTeamStatus.Failed, + skipReason: AddTeamSkipReason.NotQueued, + message: `Team "${raw}" is not in the approval queue.`, + }) + } + continue + } + const conflicts = teamsByLowerName.get(lower) || [] const sameParent = conflicts.find((team) => team.node_id === parentNodeId) if (sameParent) { @@ -229,8 +260,9 @@ export async function addTeams(auth: Auth, input: AddTeamInput): Promise HEADER_BY_COLUMN[column])] + const usersTitle = options.usersColumnTitle?.trim() || HEADER_BY_COLUMN[TeamColumn.Users] + const headers: string[] = [ + '#', + 'Company', + 'Team UID', + 'Name', + ...columns.map((column) => (column === TeamColumn.Users ? usersTitle : HEADER_BY_COLUMN[column])), + ] const outRows: string[][] = rows.map((row, rowIndex) => { - const cells: string[] = [String(rowIndex + 1), row.team_uid, row.name] + const cells: string[] = [String(rowIndex + 1), row.company ?? '', row.team_uid, row.name] for (const column of columns) cells.push(formatCell(row, column)) return cells }) @@ -230,6 +237,7 @@ function rowMatchesPattern(row: ListTeamRow, pattern: string): boolean { const lowered = pattern.toLowerCase() const tokens: string[] = [] tokens.push(row.team_uid.toLowerCase()) + if (row.company) tokens.push(...tokenize(row.company.toLowerCase())) tokens.push(...tokenize(row.name.toLowerCase())) if (row.restricts) tokens.push(row.restricts.toLowerCase()) if (row.node) tokens.push(...tokenize(row.node.toLowerCase())) diff --git a/KeeperSdk/src/teams/teamRole.ts b/KeeperSdk/src/teams/teamRole.ts new file mode 100644 index 00000000..65cf6d3c --- /dev/null +++ b/KeeperSdk/src/teams/teamRole.ts @@ -0,0 +1,201 @@ +import { type Auth, createInMessage, Enterprise, normal64Bytes } from '@keeper-security/keeperapi' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' +import { + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseRole, + type EnterpriseRoleTeamLink, +} from './enterpriseData' +import { applyDecryptedRoleNames, resolveExistingRoles } from '../roles/roleUtils' +import { resolveExistingTeams } from './teamUtils' + +const TEAM_ROLE_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.Roles, + EnterpriseDataInclude.RoleTeams, +] + +export type ChangeTeamRolesInput = { + teams: string[] + addRoles?: string[] + removeRoles?: string[] +} + +export enum TeamRoleStatus { + Added = 'added', + Removed = 'removed', + Skipped = 'skipped', + Failed = 'failed', +} + +export type TeamRoleItemResult = { + status: TeamRoleStatus + roleName: string + roleId: number + teamName: string + teamUid: string + message?: string +} + +export type TeamRoleResult = { + success: boolean + items: TeamRoleItemResult[] + succeeded: number + failed: number + skipped: number +} + +type ResolvedTeam = { team_uid: string; name: string } + +async function loadTeamRoleContext(auth: Auth, teamIds: string[]) { + const enterpriseData = new EnterpriseDataManager(auth) + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(TEAM_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]) + const roles = response.roles || [] + applyDecryptedRoleNames(roles, displayNames.roles) + const teams = resolveExistingTeams(response.teams || [], teamIds) + return { teams, allRoles: roles, roleTeams: response.role_teams || [] } +} + +function existingRoleTeamLinks(roleTeams: EnterpriseRoleTeamLink[], teamUid: string): Set { + const ids = new Set() + for (const link of roleTeams) { + if (link.team_uid === teamUid) ids.add(link.role_id) + } + return ids +} + +function expandRemoveRoles( + removeRoles: string[], + roleTeams: EnterpriseRoleTeamLink[], + teamUid: string, + allRoles: EnterpriseRole[] +): EnterpriseRole[] { + if (removeRoles.some((role) => role.trim().toLowerCase() === '@all')) { + const ids = existingRoleTeamLinks(roleTeams, teamUid) + return allRoles.filter((role) => ids.has(role.role_id)) + } + return resolveExistingRoles(allRoles, removeRoles) +} + +async function sendRoleTeamBatch( + auth: Auth, + links: Array<{ roleId: number; teamUid: string }>, + add: boolean +): Promise { + if (links.length === 0) return + const payload = Enterprise.RoleTeams.create({ + roleTeam: links.map((link) => + Enterprise.RoleTeam.create({ + roleId: link.roleId, + teamUid: normal64Bytes(link.teamUid), + }) + ), + }) + const path = add ? 'enterprise/role_team_add' : 'enterprise/role_team_remove' + const message = createInMessage(payload, path, Enterprise.RoleTeams) + await auth.executeRestAction(message) +} + +function roleDisplayName(role: { role_id: number; displayName?: string }): string { + return (role.displayName || '').trim() || String(role.role_id) +} + +export async function changeTeamRoles(auth: Auth, input: ChangeTeamRolesInput): Promise { + const teamIds = (input.teams || []).map((t) => t.trim()).filter(Boolean) + const addRoleIds = (input.addRoles || []).map((r) => r.trim()).filter(Boolean) + const removeRoleIds = (input.removeRoles || []).map((r) => r.trim()).filter(Boolean) + + if (teamIds.length === 0) { + throw new KeeperSdkError('No teams specified.', ResultCodes.NO_TEAMS_TO_UPDATE) + } + if (addRoleIds.length === 0 && removeRoleIds.length === 0) { + throw new KeeperSdkError('No roles specified.', ResultCodes.ROLE_NOT_FOUND) + } + + const ctx = await loadTeamRoleContext(auth, teamIds) + const items: TeamRoleItemResult[] = [] + const toAdd: Array<{ roleId: number; teamUid: string }> = [] + const toRemove: Array<{ roleId: number; teamUid: string }> = [] + + for (const team of ctx.teams as ResolvedTeam[]) { + const teamName = team.name || team.team_uid + const linkedRoleIds = existingRoleTeamLinks(ctx.roleTeams, team.team_uid) + + if (addRoleIds.length > 0) { + const rolesToAdd = resolveExistingRoles(ctx.allRoles, addRoleIds) + for (const role of rolesToAdd) { + const base = { + roleName: roleDisplayName(role), + roleId: role.role_id, + teamName, + teamUid: team.team_uid, + } + if (linkedRoleIds.has(role.role_id)) { + items.push({ + ...base, + status: TeamRoleStatus.Skipped, + message: 'Role is already assigned to team', + }) + continue + } + toAdd.push({ roleId: role.role_id, teamUid: team.team_uid }) + items.push({ ...base, status: TeamRoleStatus.Added }) + } + } + + if (removeRoleIds.length > 0) { + const rolesToRemove = expandRemoveRoles(removeRoleIds, ctx.roleTeams, team.team_uid, ctx.allRoles) + for (const role of rolesToRemove) { + const base = { + roleName: roleDisplayName(role), + roleId: role.role_id, + teamName, + teamUid: team.team_uid, + } + if (!linkedRoleIds.has(role.role_id)) { + items.push({ + ...base, + status: TeamRoleStatus.Skipped, + message: 'Role is not assigned to team', + }) + continue + } + toRemove.push({ roleId: role.role_id, teamUid: team.team_uid }) + items.push({ ...base, status: TeamRoleStatus.Removed }) + } + } + } + + try { + await sendRoleTeamBatch(auth, toRemove, false) + await sendRoleTeamBatch(auth, toAdd, true) + } catch (err) { + const message = extractErrorMessage(err) + for (const item of items) { + if (item.status === TeamRoleStatus.Added || item.status === TeamRoleStatus.Removed) { + item.status = TeamRoleStatus.Failed + item.message = message + } + } + } + + let succeeded = 0 + let skipped = 0 + let failed = 0 + for (const item of items) { + if (item.status === TeamRoleStatus.Added || item.status === TeamRoleStatus.Removed) succeeded++ + else if (item.status === TeamRoleStatus.Skipped) skipped++ + else failed++ + } + + return { + success: failed === 0 && succeeded > 0, + items, + succeeded, + skipped, + failed, + } +} diff --git a/KeeperSdk/src/utils/index.ts b/KeeperSdk/src/utils/index.ts index 942a340f..cd1c2fb2 100644 --- a/KeeperSdk/src/utils/index.ts +++ b/KeeperSdk/src/utils/index.ts @@ -31,3 +31,21 @@ export { escapeRegExp, resolveSearchPattern, } from './patterns' +export { + DEFAULT_PASSWORD_LENGTH, + PW_SPECIAL_CHARACTERS, + GEN_PASSWORD_ALGORITHMS, + KeeperPasswordGenerator, + generatePasswordFromOptions, + resolveGenPasswordAlgorithm, + generatePassword, + parseGenParametersFromValue, + isGenerateFieldValue, + parseGeneratePasswordFlag, +} from './passwordGenerator' +export type { + GenPasswordAlgorithm, + PasswordGenerationOptions, + PasswordComplexityPolicy, + PassphraseGenOptions, +} from './passwordGenerator' diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index a9dc6143..b2395a58 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -8,11 +8,14 @@ import { DTeam, DUserFolder, Authentication, + normal64Bytes, + platform, } from '@keeper-security/keeperapi' import type { SyncResult, SyncLogFormat, VaultStorage, SessionStorage, AuthUI3 } from '@keeper-security/keeperapi' import { InMemoryStorage } from '../storage/InMemoryStorage' import { SessionManager } from '../auth/SessionManager' -import { ConsoleAuthUI } from '../auth/ConsoleAuthUI' +import { getSdkPlatform } from '../platform' +import { toSessionParams, type SessionRestoreInput } from '../auth/sessionRestore' import { searchRecords, formatRecord, getRecordTitle, getRecordType } from '../records/RecordUtils' import { addRecord as addRecordOp, @@ -21,6 +24,16 @@ import { getRecordHistory as getRecordHistoryOp, moveRecord as moveRecordOp, } from '../records/RecordOperations' +import { + EnterpriseReportManager, + runPasswordReport, + type AuditReportOptions, + type AuditReportResult, + type ActionReportOptions, + type ActionReportResult, + type PasswordReportOptions, + type PasswordReportResult, +} from '../enterpriseReport' import type { NewRecordInput, TypedRecordData, @@ -69,6 +82,7 @@ import type { TeamView } from '../teams/viewTeam' import type { AddTeamInput, AddTeamResult } from '../teams/addTeam' import type { UpdateTeamInput, UpdateTeamResult } from '../teams/updateTeam' import type { DeleteTeamInput, DeleteTeamResult } from '../teams/deleteTeam' +import type { ChangeTeamRolesInput, TeamRoleResult } from '../teams/teamRole' import { RoleManager, type AddRoleInput, @@ -80,40 +94,44 @@ import { type RoleView, type UpdateRoleInput, type UpdateRoleResult, + type SetRoleEnforcementsInput, + type SetRoleEnforcementsResult, + type FormattedRoleEnforcementTable, + type CopyRoleInput, + type CopyRoleResult, + type FormattedCopyRoleTable, + type AddUsersToRolesInput, + type RemoveUsersFromRolesInput, + type RoleUserResult, + type FormattedRoleUserTable, + type ManageRoleNodesInput, + type ManageRoleNodesResult, + type FormattedManageRoleNodesTable, + type ChangeRolePrivilegesInput, + type ChangeRolePrivilegesResult, + type FormattedRolePrivilegeTable, } from '../roles' import { - EnterpriseReportManager, - runPasswordReport, - type AuditReportOptions, - type AuditReportResult, - type ActionReportOptions, - type ActionReportResult, - type PasswordReportOptions, - type PasswordReportResult, -} from '../enterpriseReport' + NodeManager, + type AddNodeInput, + type AddNodeResult, + type DeleteNodeInput, + type DeleteNodeResult, + type ListNodeRow, + type ListNodesOptions, + type NodeView, + type UpdateNodeInput, + type UpdateNodeResult, +} from '../nodes' import { UserManager } from '../users/UserManager' import { NestedShareFolderManager } from '../nestedShareFolders/NestedShareFolderManager' -import { isNestedShareFolder } from '../nestedShareFolders/nsfHelpers' -import { formatListNsfTable, renderListNsfAsciiTable, formatListNsfOutput } from '../nestedShareFolders/listNsf' -import { formatNsfDetail } from '../nestedShareFolders/getNsf' -import { formatRemoveNsfPreview } from '../nestedShareFolders/removeNsfRecord' -import { - formatNsfRecordSharePlan, - formatNsfRecordShareResults, - formatNsfFolderShareResults, -} from '../nestedShareFolders/nsfShare' -import { - formatNsfRecordPermissionPlan, - formatNsfRecordPermissionRequestHeader, - formatNsfRecordPermissionFailures, -} from '../nestedShareFolders/nsfRecordPermission' -import { formatNsfShortcutOutput, formatKeepNsfShortcutPlan } from '../nestedShareFolders/nsfShortcut' -import { formatTransferNestedShareRecordResults } from '../nestedShareFolders/nsfTransferRecord' +import { isNestedShareFolder, isRootFolderUid } from '../nestedShareFolders/nsfHelpers' import type { AddNsfRecordInput, AddNsfRecordResult, AddNsfRecordsInput, AddNsfRecordsResult, + FormattedListNsfTable, GetNsfOptions, GetNsfResult, GetNsfRecordDetailsInput, @@ -124,7 +142,6 @@ import type { ListNsfFormatInput, ListNsfOptions, ListNsfRow, - FormattedListNsfTable, ListNsfShortcutsOptions, MkdirNsfInput, MkdirNsfResult, @@ -139,8 +156,6 @@ import type { ShareNestedShareRecordResult, TransferNestedShareRecordInput, TransferNestedShareRecordResult, - UpdateNsfFolderInput, - UpdateNsfFolderResult, UpdateNsfRecordInput, UpdateNsfRecordItemInput, UpdateNsfRecordsInput, @@ -149,6 +164,12 @@ import type { UpdateNsfRecordResult, UpdateNsfRecordResultItem, } from '../nestedShareFolders/nsfTypes' +import type { + UpdateNsfFolderBatchItem, + UpdateNsfFolderInput, + UpdateNsfFolderResult, + UpdateNsfFoldersResult, +} from '../nestedShareFolders/updateNsfFolder' import type { ListUserRow, ListUsersOptions, @@ -173,7 +194,21 @@ import type { TeamUserResult, FormattedTeamUserTable, } from '../users/userTypes' -import { ConsoleLogger, LogLevel, KeeperSdkError, extractErrorMessage, SdkDefaults, ResultCodes } from '../utils' +import type { + UpdateUsersOnTeamsInput, + UpdateUsersOnTeamsResult, + FormattedUpdateTeamUserTable, +} from '../users/updateTeamUser' +import { buildWhoamiInfo, type WhoamiInfo } from '../account/whoamiInfo' +import { + ConsoleLogger, + LogLevel, + KeeperSdkError, + extractErrorMessage, + extractResultCode, + SdkDefaults, + ResultCodes, +} from '../utils' import type { ILogger } from '../utils' enum VaultStatus { @@ -210,11 +245,13 @@ export class KeeperVault { private readonly log: ILogger private synced = false private batchDepth = 0 + private restoredAccountUid: string | null = null private readonly folderSession: VaultFolderSession = FolderManager.createSession() private readonly folderManager: FolderManager private readonly sharedFolderManager: SharedFolderManager private readonly teamManager: TeamManager private readonly roleManager: RoleManager + private readonly nodeManager: NodeManager private readonly enterpriseReportManager: EnterpriseReportManager private readonly userManager: UserManager private readonly nestedShareFolderManager: NestedShareFolderManager @@ -233,13 +270,14 @@ export class KeeperVault { this.log = new ConsoleLogger(this.config.logLevel) this.storage = config?.storage || new InMemoryStorage() this.sessionManager = config?.sessionStorage || new SessionManager(this.config.configDir || undefined) - this.authUI = config?.authUI || new ConsoleAuthUI() + this.authUI = config?.authUI ?? getSdkPlatform().createAuthUI(this.config.useConsoleAuth) const authProvider = () => this.getAuthOrThrow() this.folderManager = new FolderManager(this.storage, this.folderSession, authProvider) this.sharedFolderManager = new SharedFolderManager(this.storage, authProvider) this.teamManager = new TeamManager(authProvider) this.roleManager = new RoleManager(authProvider) + this.nodeManager = new NodeManager(authProvider) this.enterpriseReportManager = new EnterpriseReportManager(authProvider) this.userManager = new UserManager(authProvider) this.nestedShareFolderManager = new NestedShareFolderManager(this.storage, authProvider) @@ -269,16 +307,6 @@ export class KeeperVault { return this.enterpriseReportManager } - /** @deprecated Use getEnterpriseReportManager() */ - public getAuditReportManager(): EnterpriseReportManager { - return this.enterpriseReportManager - } - - /** @deprecated Use getEnterpriseReportManager() */ - public getActionReportManager(): EnterpriseReportManager { - return this.enterpriseReportManager - } - private async createAuth(options?: { useSessionResumption?: boolean }): Promise { const host = this.config.host const baseDeviceConfig = await this.sessionManager.getDeviceConfig(host) @@ -361,10 +389,106 @@ export class KeeperVault { this.log.info(`Logged in as ${username} (via session token)`) } + /** + * Persist device token and private key for this vault host in session storage so + * {@link loginWithSessionToken} and {@link resumeSession} work without a prior password login on this machine. + * Values use the same base64 / base64url decoding as {@link SessionManager} (`normal64Bytes`). + */ + public async registerDevice( + deviceToken: string, + privateKey: string, + options?: { username?: string } + ): Promise { + const host = this.config.host + const save = this.sessionManager.createOnDeviceConfig(host) + await save({ + deviceToken: normal64Bytes(deviceToken), + privateKey: normal64Bytes(privateKey), + }) + if (options?.username) { + this.sessionManager.setLastUsername(options.username) + } + this.log.info(`Device credentials stored for host ${host}`) + } + public getSessionToken(): string | undefined { return this.auth?.sessionToken || undefined } + /** + * Resume a session from extension-exported {@link SessionRestoreInput}. + * Verifies the token with a lightweight server call so an expired session + * fails here, not later from `sync()`. + */ + public async restoreSession(input: SessionRestoreInput): Promise { + const params = toSessionParams(input) + await this.sessionManager.saveSessionParameters(params) + this.sessionManager.setLastUsername(params.username) + + this.auth = await this.createAuth() + await this.auth.continueSession() + + if (!this.auth.sessionToken) { + throw new KeeperSdkError( + 'Session restore failed — session token may be expired or invalid.', + ResultCodes.SESSION_TOKEN_EXPIRED + ) + } + + try { + await this.auth.loadAccountSummary() + } catch (err) { + const code = extractResultCode(err) + const msg = extractErrorMessage(err) + this.disconnect() + const isExpired = code === ResultCodes.SESSION_TOKEN_EXPIRED + throw new KeeperSdkError( + isExpired + ? `Session token rejected by server (${code}): ${msg}. Re-export session JSON and try again.` + : `Session restore failed: ${msg}`, + code ?? ResultCodes.SESSION_TOKEN_EXPIRED + ) + } + + const accountUid = input.accountUid.trim() + if (this.restoredAccountUid && accountUid && this.restoredAccountUid !== accountUid) { + await this.storage.clear() + platform.unloadKeys() + } else { + // Preserve vault data and continuation token for incremental sync on the same account. + platform.unloadNonUserKeys() + } + if (accountUid) { + this.restoredAccountUid = accountUid + } + this.synced = false + this.log.info(`Session restored for ${params.username}`) + } + + public async getAccountUsername(): Promise { + return this.sessionManager.getLastUsername() ?? this.auth?.username ?? undefined + } + + public async getWhoamiInfo(options?: { includeVaultCounts?: boolean }): Promise { + const auth = this.getAuthOrThrow() + if (!auth.accountSummary) { + await auth.loadAccountSummary() + } + const summary = auth.accountSummary + if (!summary) { + throw new KeeperSdkError('Account summary is unavailable.', ResultCodes.SYNC_FAILED) + } + + const username = auth.username || (await this.getAccountUsername()) || '' + + return buildWhoamiInfo({ + username, + host: this.host, + accountSummary: summary, + vaultSummary: options?.includeVaultCounts ? this.getSummary() : undefined, + }) + } + public async resumeSession(): Promise { const username = await this.sessionManager.getLastUsername() if (!username) { @@ -413,14 +537,32 @@ export class KeeperVault { public async sync(): Promise { const auth = this.getAuthOrThrow() - const result = await syncDown({ - auth, - storage: this.storage, - logFormat: this.config.logFormat, - }) + try { + const result = await syncDown({ + auth, + storage: this.storage, + logFormat: this.config.logFormat, + }) + if (result.error) { + this.log.error('Sync error:', result.error) + throw new KeeperSdkError(`Sync failed: ${result.error}`, ResultCodes.SYNC_FAILED) + } + this.synced = true + return result + } catch (e) { + this.log.error('Sync failed:', extractErrorMessage(e)) + throw e + } + } - this.synced = true - return result + /** Drop incremental sync cursor and crypto caches so the next sync can restart cleanly. */ + public async clearSyncCheckpoint(): Promise { + const checkpoint = await this.storage.get('continuationToken') + if (checkpoint?.token) { + await this.storage.delete('continuationToken', checkpoint.token) + } + platform.unloadNonUserKeys() + this.synced = false } public async batch(fn: () => Promise): Promise { @@ -441,7 +583,16 @@ export class KeeperVault { return } if (this.config.autoSync) { - await this.sync() + try { + await this.sync() + } catch (err) { + this.synced = false + const msg = extractErrorMessage(err) + if (!/unable to load the key/i.test(msg)) { + // TODO: handle this case based on commander fix + this.log.warn(`Post-mutation sync failed: ${msg}`) + } + } } else { this.synced = false } @@ -452,7 +603,10 @@ export class KeeperVault { } public getRecordByUid(uid: string): DRecord | undefined { - return this.storage.getByUid(VaultObjectKind.Record, uid) + const direct = this.storage.getByUid(VaultObjectKind.Record, uid) + if (direct) return direct + const lower = uid.toLowerCase() + return this.getRecords().find((record) => record.uid?.toLowerCase() === lower) } public findRecord(uidOrTitle: string): DRecord | undefined { @@ -563,6 +717,24 @@ export class KeeperVault { return this.userManager.removeUsersFromTeams(input) } + public async updateUsersOnTeams(input: UpdateUsersOnTeamsInput): Promise { + return this.userManager.updateUsersOnTeams(input) + } + + public formatUpdateTeamUserResult(result: UpdateUsersOnTeamsResult): FormattedUpdateTeamUserTable { + return this.userManager.formatUpdateTeamUserResult(result) + } + + public renderUpdateTeamUserAsciiTable(table: FormattedUpdateTeamUserTable): string { + return this.userManager.renderUpdateTeamUserAsciiTable(table) + } + + public async changeTeamRoles(input: ChangeTeamRolesInput): Promise { + const result = await this.teamManager.changeTeamRoles(input) + if (result.succeeded > 0) await this.syncIfNeeded() + return result + } + public formatTeamUserResult(result: TeamUserResult): FormattedTeamUserTable { return this.userManager.formatTeamUserResult(result) } @@ -615,6 +787,92 @@ export class KeeperVault { return result } + public async setRoleEnforcements(input: SetRoleEnforcementsInput): Promise { + return this.roleManager.setRoleEnforcements(input) + } + + public formatSetRoleEnforcementsResult(result: SetRoleEnforcementsResult): FormattedRoleEnforcementTable { + return this.roleManager.formatSetRoleEnforcementsResult(result) + } + + public renderRoleEnforcementAsciiTable(table: FormattedRoleEnforcementTable): string { + return this.roleManager.renderRoleEnforcementAsciiTable(table) + } + + public async copyRoles(input: CopyRoleInput): Promise { + const result = await this.roleManager.copyRoles(input) + if (result.success) await this.syncIfNeeded() + return result + } + + public formatCopyRoleResult(result: CopyRoleResult): FormattedCopyRoleTable { + return this.roleManager.formatCopyRoleResult(result) + } + + public renderCopyRoleAsciiTable(table: FormattedCopyRoleTable): string { + return this.roleManager.renderCopyRoleAsciiTable(table) + } + + public async addUsersToRoles(input: AddUsersToRolesInput): Promise { + return this.roleManager.addUsersToRoles(input) + } + + public async removeUsersFromRoles(input: RemoveUsersFromRolesInput): Promise { + return this.roleManager.removeUsersFromRoles(input) + } + + public formatRoleUserResult(result: RoleUserResult): FormattedRoleUserTable { + return this.roleManager.formatRoleUserResult(result) + } + + public renderRoleUserAsciiTable(table: FormattedRoleUserTable): string { + return this.roleManager.renderRoleUserAsciiTable(table) + } + + public async manageRoleNodes(input: ManageRoleNodesInput): Promise { + return this.roleManager.manageRoleNodes(input) + } + + public formatManageRoleNodesResult(result: ManageRoleNodesResult): FormattedManageRoleNodesTable { + return this.roleManager.formatManageRoleNodesResult(result) + } + + public renderManageRoleNodesAsciiTable(table: FormattedManageRoleNodesTable): string { + return this.roleManager.renderManageRoleNodesAsciiTable(table) + } + + public async changeRolePrivileges(input: ChangeRolePrivilegesInput): Promise { + return this.roleManager.changeRolePrivileges(input) + } + + public formatChangeRolePrivilegesResult(result: ChangeRolePrivilegesResult): FormattedRolePrivilegeTable { + return this.roleManager.formatChangeRolePrivilegesResult(result) + } + + public renderChangeRolePrivilegesAsciiTable(table: FormattedRolePrivilegeTable): string { + return this.roleManager.renderChangeRolePrivilegesAsciiTable(table) + } + + public async listNodes(options?: ListNodesOptions): Promise { + return this.nodeManager.listNodes(options ?? {}) + } + + public async viewNode(identifier: string): Promise { + return this.nodeManager.viewNode(identifier) + } + + public async addNodes(input: AddNodeInput): Promise { + return this.nodeManager.addNodes(input) + } + + public async updateNodes(input: UpdateNodeInput): Promise { + return this.nodeManager.updateNodes(input) + } + + public async deleteNodes(input: DeleteNodeInput): Promise { + return this.nodeManager.deleteNodes(input) + } + public async runAuditReport(options?: AuditReportOptions): Promise { return this.enterpriseReportManager.runAuditReport(options ?? {}) } @@ -631,6 +889,10 @@ export class KeeperVault { return this.folderManager.changeDirectory(path) } + public async tryResolvePath(path: string): Promise { + return this.folderManager.tryResolvePath(path) + } + public getCurrentFolderUid(): string | null { return this.folderManager.getCurrentFolderUid() } @@ -684,13 +946,19 @@ export class KeeperVault { confirm?: (summary: string) => boolean | Promise ): Promise { const result = await this.folderManager.deleteFolder(folderRefs, confirm) - if (result.success) await this.syncIfNeeded() + if (result.success) { + await this.clearSyncCheckpoint() + await this.syncIfNeeded() + } return result } public async rmdir(patterns: string[], options?: RmdirOptions): Promise { const result = await this.folderManager.rmdir(patterns, options ?? {}) - if (result.success) await this.syncIfNeeded() + if (result.success) { + await this.clearSyncCheckpoint() + await this.syncIfNeeded() + } return result } @@ -846,15 +1114,15 @@ export class KeeperVault { } public formatListNsfTable(rows: ListNsfRow[], options?: { columnWidth?: number }): FormattedListNsfTable { - return formatListNsfTable(rows, options ?? {}) + return this.nestedShareFolderManager.formatListNsfTable(rows, options ?? {}) } public renderListNsfAsciiTable(table: FormattedListNsfTable, options?: { minColWidth?: number }): string { - return renderListNsfAsciiTable(table, options ?? {}) + return this.nestedShareFolderManager.renderListNsfAsciiTable(table, options ?? {}) } public formatListNsfOutput(rows: ListNsfRow[], format?: ListNsfFormatInput): string { - return formatListNsfOutput(rows, format) + return this.nestedShareFolderManager.formatListNsfOutput(rows, format) } public async getNestedShareFolder(identifier: string, options?: GetNsfOptions): Promise { @@ -862,7 +1130,7 @@ export class KeeperVault { } public formatNsfDetail(result: GetNsfResult, verbose?: boolean): string { - return formatNsfDetail(result, verbose ?? false) + return this.nestedShareFolderManager.formatNsfDetail(result, verbose ?? false) } public async linkNestedShareRecord( @@ -881,7 +1149,7 @@ export class KeeperVault { } public formatRemoveNsfPreview(preview: RemoveNsfRecordResult['preview']): string { - return formatRemoveNsfPreview(preview) + return this.nestedShareFolderManager.formatRemoveNsfPreview(preview) } public async mkdirNestedShareFolder(input: Omit): Promise { @@ -895,48 +1163,18 @@ export class KeeperVault { return result } - public async removeNestedShareFolders(input: RemoveNsfFolderInput): Promise { - const result = await this.nestedShareFolderManager.removeNestedShareFolders(input) - if (result.confirmed) await this.syncIfNeeded() - return result - } - - public async getNestedShareRecordDetails(input: GetNsfRecordDetailsInput): Promise { - return this.nestedShareFolderManager.getNestedShareRecordDetails(input) - } - - public async updateNestedShareRecords( - input: UpdateNsfRecordInput | UpdateNsfRecordsInput - ): Promise { - const result = await this.nestedShareFolderManager.updateNestedShareRecords(input) - if (result.updated.some((item) => item.success)) await this.syncIfNeeded() - return result - } - - public async updateNestedShareRecord(input: UpdateNsfRecordItemInput): Promise { - const result = await this.nestedShareFolderManager.updateNestedShareRecord(input) - if (result.success) await this.syncIfNeeded() - return result - } - - public async addNestedShareRecords(input: AddNsfRecordsInput): Promise { - const result = await this.nestedShareFolderManager.addNestedShareRecords(input) - if (result.added.some((item) => item.success)) await this.syncIfNeeded() - return result - } - - public async addNestedShareRecord(input: AddNsfRecordInput): Promise { - const result = await this.nestedShareFolderManager.addNestedShareRecord(input) - if (result.success) await this.syncIfNeeded() - return result - } - public async updateNestedShareFolder(input: UpdateNsfFolderInput): Promise { const result = await this.nestedShareFolderManager.updateNestedShareFolder(input) if (result.updated) await this.syncIfNeeded() return result } + public async updateNestedShareFolders(updates: UpdateNsfFolderBatchItem[]): Promise { + const result = await this.nestedShareFolderManager.updateNestedShareFolders(updates) + if (result.anyUpdated) await this.syncIfNeeded() + return result + } + public async shareNestedShareFolder(input: ShareNestedShareFolderInput): Promise { const result = await this.nestedShareFolderManager.shareNestedShareFolder(input) if (result.results.some((item) => item.success)) await this.syncIfNeeded() @@ -945,53 +1183,114 @@ export class KeeperVault { public async shareNestedShareRecord(input: ShareNestedShareRecordInput): Promise { const result = await this.nestedShareFolderManager.shareNestedShareRecord(input) - if (!result.dryRun && result.results.some((item) => item.success)) await this.syncIfNeeded() + if (!result.dryRun && result.results.some((item) => item.success)) { + await this.syncIfNeeded() + } return result } public formatNsfRecordSharePlan(result: ShareNestedShareRecordResult): string { - return formatNsfRecordSharePlan(result.plan, result.dryRun) + return this.nestedShareFolderManager.formatNsfRecordSharePlan(result) } public formatNsfRecordShareResults(results: ShareNestedShareRecordResult['results']): string { - return formatNsfRecordShareResults(results) - } - - public formatNsfFolderShareResults(results: ShareNestedShareFolderResult['results']): string { - return formatNsfFolderShareResults(results) + return this.nestedShareFolderManager.formatNsfRecordShareResults(results) } - public listNsfShortcuts(options?: ListNsfShortcutsOptions): NsfShortcutRow[] { - this.getAuthOrThrow() - return this.nestedShareFolderManager.listNsfShortcuts(options ?? {}) + public listNsfShortcuts(options: ListNsfShortcutsOptions = {}): NsfShortcutRow[] { + return this.nestedShareFolderManager.listNsfShortcuts(options) } public formatNsfShortcutOutput(rows: NsfShortcutRow[], format?: ListNsfShortcutsOptions['format']): string { - return formatNsfShortcutOutput(rows, format) + return this.nestedShareFolderManager.formatNsfShortcutOutput(rows, format) } public async keepNsfShortcut(input: KeepNsfShortcutInput): Promise { const current = this.folderSession.currentFolderUid const defaultFolderUid = current && isNestedShareFolder(this.storage, current) ? current : undefined const result = await this.nestedShareFolderManager.keepNsfShortcut(input, defaultFolderUid) - if (!result.dryRun && result.results.some((item) => item.success)) await this.syncIfNeeded() + if (!result.dryRun && !result.nothingToDo && result.results.some((item) => item.success)) { + await this.syncIfNeeded() + } return result } public formatKeepNsfShortcutPlan(result: KeepNsfShortcutResult): string { - return formatKeepNsfShortcutPlan(result.plan) + return this.nestedShareFolderManager.formatKeepNsfShortcutPlan(result) } public async transferNestedShareRecords( input: TransferNestedShareRecordInput ): Promise { const result = await this.nestedShareFolderManager.transferNestedShareRecords(input) - if (result.results.some((item) => item.success)) await this.syncIfNeeded() + if (result.success) await this.syncIfNeeded() return result } public formatTransferNestedShareRecordResults(results: TransferNestedShareRecordResult['results']): string { - return formatTransferNestedShareRecordResults(results) + return this.nestedShareFolderManager.formatTransferNestedShareRecordResults(results) + } + + public async removeNestedShareFolders(input: RemoveNsfFolderInput): Promise { + const result = await this.nestedShareFolderManager.removeNestedShareFolders(input) + if (result.confirmed) await this.syncIfNeeded() + return result + } + + public formatRemoveNsfFolderPreview( + preview: RemoveNsfFolderResult['preview'], + operation: RemoveNsfFolderResult['operation'], + quiet?: boolean + ): string { + return this.nestedShareFolderManager.formatRemoveNsfFolderPreview(preview, operation, quiet) + } + + public async getNestedShareRecordDetails(input: GetNsfRecordDetailsInput): Promise { + return this.nestedShareFolderManager.getNestedShareRecordDetails(input) + } + + public formatNsfRecordDetailsOutput( + result: GetNsfRecordDetailsResult, + format?: GetNsfRecordDetailsInput['format'] + ): string { + return this.nestedShareFolderManager.formatNsfRecordDetailsOutput(result, format) + } + + public async updateNestedShareRecords( + input: UpdateNsfRecordInput | UpdateNsfRecordsInput + ): Promise { + const result = await this.nestedShareFolderManager.updateNestedShareRecords(input) + if (result.updated.some((item) => item.success)) await this.syncIfNeeded() + return result + } + + public async updateNestedShareRecord(input: UpdateNsfRecordItemInput): Promise { + const result = await this.nestedShareFolderManager.updateNestedShareRecord(input) + if (result.success) await this.syncIfNeeded() + return result + } + + public async addNestedShareRecords(input: AddNsfRecordsInput): Promise { + const current = this.folderSession.currentFolderUid + const defaultFolder = + current && isNestedShareFolder(this.storage, current) && !isRootFolderUid(this.storage, current) + ? current + : undefined + const records = (input.records ?? []).map((record) => { + if (record.folder?.trim()) return record + return defaultFolder ? { ...record, folder: defaultFolder } : record + }) + const result = await this.nestedShareFolderManager.addNestedShareRecords({ ...input, records }) + if (result.added.some((item) => item.success)) await this.syncIfNeeded() + return result + } + + public async addNestedShareRecord(input: AddNsfRecordInput): Promise { + const { added } = await this.addNestedShareRecords({ records: [input] }) + if (!added[0]) { + throw new KeeperSdkError('Failed to add nested share record.', ResultCodes.NSF_ADD_FAILED) + } + return added[0] } public async updateNestedShareRecordPermissions( @@ -1003,23 +1302,14 @@ export class KeeperVault { } public formatNsfRecordPermissionPlan(result: UpdateNsfRecordPermissionResult): string { - return formatNsfRecordPermissionPlan(result.plan) - } - - public formatNsfRecordPermissionRequestHeader( - action: UpdateNsfRecordPermissionInput['action'], - role: string | undefined, - folderDisplayName: string, - recursive: boolean - ): string { - return formatNsfRecordPermissionRequestHeader(action, role, folderDisplayName, recursive) + return this.nestedShareFolderManager.formatNsfRecordPermissionPlan(result) } public formatNsfRecordPermissionFailures( failures: UpdateNsfRecordPermissionResult['grantFailures'], kind: 'GRANT' | 'REVOKE' ): string { - return formatNsfRecordPermissionFailures(failures, kind) + return this.nestedShareFolderManager.formatNsfRecordPermissionFailures(failures, kind) } public async shareFolder(input: ShareFolderInput): Promise { diff --git a/README.md b/README.md index 0732d0a1..cce3ad21 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ This repository contains npm packages for interacting with the Keeper backend from JavaScript or TypeScript. -[`@keeper-security/keeper-sdk-javascript`](KeeperSdk) has an easy-to-use API with high-level helpers for auth, records, folders, sharing, and teams, plus runnable examples. - -[`@keeper-security/keeperapi`](keeperapi) is the underlying core API client. Direct use is not recommended. +| Package | Purpose | +|---------|---------| +| [`@keeper-security/keeper-sdk-javascript`](KeeperSdk) | High-level API + Commander-style in-process CLI (`dispatchCliLine`) | +| [`@keeper-security/keeperapi`](keeperapi) | Low-level REST/protobuf client (direct use not recommended) | [![keeper-sdk-javascript on npm](https://img.shields.io/npm/v/@keeper-security/keeper-sdk-javascript?label=%40keeper-security%2Fkeeper-sdk-javascript&style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@keeper-security/keeper-sdk-javascript) [![keeperapi on npm](https://img.shields.io/npm/v/@keeper-security/keeperapi?label=%40keeper-security%2Fkeeperapi&style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@keeper-security/keeperapi) @@ -14,20 +15,45 @@ This repository contains npm packages for interacting with the Keeper backend fr ## Repository layout ``` -KeeperSdk/ # JS SDK (@keeper-security/keeper-sdk-javascript) -keeperapi/ # core API client (@keeper-security/keeperapi) -examples/ -└── sdk_example/ # runnable scripts demonstrating the SDK +keeper-sdk-javascript/ +├── KeeperSdk/ # @keeper-security/keeper-sdk-javascript +├── keeperapi/ # @keeper-security/keeperapi +└── examples/ + └── sdk_example/ # Runnable Node scripts (auth, records, folders, …) +``` + +**Start here for CLI / vault behavior:** [`KeeperSdk/README.md`](KeeperSdk/README.md) — built-in commands, `get` / `whoami` output, and `KeeperCliHost` adapter requirements. + +Browser embedders (e.g. `@keeper-security/keeper-shell-component`) consume this repo via npm or a local path. Fix CLI formatting and vault surface issues in **KeeperSdk first**, then rebuild the shell against the updated SDK. + +## Quick development + +Build **keeperapi** before **KeeperSdk** (SDK depends on keeperapi): + +```bash +cd keeperapi && npm install && npm run build +cd ../KeeperSdk && npm install && npm run link-local && npm run build +``` + +- **Node** entry: `KeeperSdk/dist/index.js` +- **Browser** entry: `KeeperSdk/dist/browser.js` (via `src/browser.ts` — no Node `readline` / console auth) + +Run SDK examples: + +```bash +cd examples/sdk_example && npm install +npm run auth:restore-session -- --from-json /path/to/session.json +npm run records:list:shell-cli -- --from-json /path/to/session.json ``` -See each package's README for installation and usage: +## Package docs -- [`KeeperSdk/README.md`](KeeperSdk/README.md) -- [`keeperapi/README.md`](keeperapi/README.md) +- [`KeeperSdk/README.md`](KeeperSdk/README.md) — API, CLI commands, host adapter +- [`keeperapi/README.md`](keeperapi/README.md) — core client ## Contributing -To ignore formatting-only commits in `git blame`, run: +To ignore formatting-only commits in `git blame`: ```bash git config blame.ignoreRevsFile .git-blame-ignore-revs diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index 9621f099..a98d9881 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -31,6 +31,11 @@ "roles:add": "ts-node src/roles/addRole.ts", "roles:update": "ts-node src/roles/updateRole.ts", "roles:delete": "ts-node src/roles/deleteRole.ts", + "roles:enforcements": "ts-node src/roles/roleEnforcements.ts", + "roles:copy": "ts-node src/roles/copyRole.ts", + "roles:role-users": "ts-node src/roles/roleUsers.ts", + "roles:manage-node": "ts-node src/roles/manageNode.ts", + "roles:privileges": "ts-node src/roles/rolePrivileges.ts", "nsf:list": "ts-node src/nestedShareFolders/list_nsf.ts", "nsf:get": "ts-node src/nestedShareFolders/get_nsf.ts", "nsf:ln": "ts-node src/nestedShareFolders/link_nsf.ts", @@ -59,12 +64,19 @@ "users:action": "ts-node src/users/action_user.ts", "users:alias": "ts-node src/users/alias_user.ts", "users:user-team": "ts-node src/users/team_user.ts", + "users:update-team-user": "ts-node src/users/update_team_user.ts", "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", "link-local": "cd ../../KeeperSdk && npm link ../keeperapi && cd ../examples/sdk_example && npm link ../../keeperapi", "types": "tsc --watch", - "types:ci": "tsc" + "types:ci": "tsc", + "nsf:folder": "ts-node src/nestedShareFolders/nsf_folder.ts", + "nsf:rndir": "ts-node src/nestedShareFolders/rndir_nsf.ts", + "nsf:share": "ts-node src/nestedShareFolders/share_nsf.ts", + "nsf:shortcut": "ts-node src/nestedShareFolders/shortcut_nsf.ts", + "nsf:transfer-record": "ts-node src/nestedShareFolders/transfer_nsf_record.ts", + "nsf:record-permission": "ts-node src/nestedShareFolders/nsf_record_permission.ts" }, "dependencies": { "@keeper-security/keeperapi": "17.1.0", diff --git a/examples/sdk_example/src/roles/copyRole.ts b/examples/sdk_example/src/roles/copyRole.ts new file mode 100644 index 00000000..e5f59aff --- /dev/null +++ b/examples/sdk_example/src/roles/copyRole.ts @@ -0,0 +1,59 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { CopyRoleResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +async function copyRoleExample() { + const vault = await login() + + try { + const source = (await prompt('Source role name or ID to copy: ')).trim() + if (!source) { + logger.error('A source role is required.') + process.exitCode = 1 + return + } + + const nameRaw = (await prompt('New role name (Enter for " Copy"): ')).trim() + const name = nameRaw || undefined + + const parentRaw = (await prompt('Parent node name or ID (Enter to reuse source role node): ')).trim() + const parent: string | null = parentRaw || null + + const clone = isYes( + await prompt('Also clone role users and teams onto the new role? [y/N]: ') + ) + + const restore = suppressLogs() + let result: CopyRoleResult + try { + result = await vault.copyRoles({ source, name, parent, clone }) + } finally { + restore() + } + + const table = vault.formatCopyRoleResult(result) + logger.info('') + logger.info(vault.renderCopyRoleAsciiTable(table)) + logger.info('') + logger.info(`Result: ${result.success ? 'success' : 'failed'}`) + + if (!result.success) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(copyRoleExample) diff --git a/examples/sdk_example/src/roles/manageNode.ts b/examples/sdk_example/src/roles/manageNode.ts new file mode 100644 index 00000000..5fa8f9e4 --- /dev/null +++ b/examples/sdk_example/src/roles/manageNode.ts @@ -0,0 +1,113 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { + ManageRoleNodesAction, + ManageRoleNodesResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { splitCommaSeparated } from '../utils/format' + +enum ManageNodeMenuChoice { + Add = '1', + Update = '2', + Remove = '3', +} + +const ACTION_BY_CHOICE: Record = { + [ManageNodeMenuChoice.Add]: 'add', + [ManageNodeMenuChoice.Update]: 'update', + [ManageNodeMenuChoice.Remove]: 'remove', +} + +function parseCascade(raw: string): 'on' | 'off' | null | undefined { + const value = raw.trim().toLowerCase() + if (value === '' || value === 'skip') return undefined + if (value === 'on') return 'on' + if (value === 'off') return 'off' + return null +} + +async function manageNodeExample() { + const vault = await login() + + try { + logger.info('') + logger.info('Select an action:') + logger.info(` ${ManageNodeMenuChoice.Add}) Add role(s) as manager of node(s)`) + logger.info(` ${ManageNodeMenuChoice.Update}) Update cascade setting for existing role/node link(s)`) + logger.info(` ${ManageNodeMenuChoice.Remove}) Remove role(s) as manager of node(s)`) + logger.info('') + + const choice = ( + await prompt(`Action [${ManageNodeMenuChoice.Add}-${ManageNodeMenuChoice.Remove}]: `) + ).trim() as ManageNodeMenuChoice + const action = ACTION_BY_CHOICE[choice] + if (!action) { + logger.error('Invalid choice.') + process.exitCode = 1 + return + } + + const rolesRaw = (await prompt('Role name(s) or ID(s) (comma-separated): ')).trim() + const roles = splitCommaSeparated(rolesRaw) + if (roles.length === 0) { + logger.error('At least one role name or ID is required.') + process.exitCode = 1 + return + } + + const nodesRaw = (await prompt('Node name(s) or ID(s) (comma-separated): ')).trim() + const nodes = splitCommaSeparated(nodesRaw) + if (nodes.length === 0) { + logger.error('At least one node name or ID is required.') + process.exitCode = 1 + return + } + + let cascade: 'on' | 'off' | null | undefined + if (action !== 'remove') { + cascade = parseCascade( + await prompt('Cascade node management to sub-nodes? [on/off, Enter to skip]: ') + ) + if (cascade === null) { + logger.error('Invalid cascade value. Use on, off, or leave blank.') + process.exitCode = 1 + return + } + } + + const restore = suppressLogs() + let result: ManageRoleNodesResult + try { + result = await vault.manageRoleNodes({ roles, nodes, action, cascade }) + } finally { + restore() + } + + const table = vault.formatManageRoleNodesResult(result) + logger.info('') + logger.info(vault.renderManageRoleNodesAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(succeeded=${result.succeeded}, skipped=${result.skipped}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.succeeded === 0)) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(manageNodeExample) diff --git a/examples/sdk_example/src/roles/roleEnforcements.ts b/examples/sdk_example/src/roles/roleEnforcements.ts new file mode 100644 index 00000000..5abc416d --- /dev/null +++ b/examples/sdk_example/src/roles/roleEnforcements.ts @@ -0,0 +1,63 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { SetRoleEnforcementsResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { splitCommaSeparated } from '../utils/format' + +async function roleEnforcementsExample() { + const vault = await login() + + try { + const rolesRaw = (await prompt('Role name(s) or ID(s) (comma-separated): ')).trim() + const roles = splitCommaSeparated(rolesRaw) + if (roles.length === 0) { + logger.error('At least one role name or ID is required.') + process.exitCode = 1 + return + } + + const enforcementsRaw = ( + await prompt('Enforcements to apply (KEY:VALUE, comma-separated; VALUE=false removes): ') + ).trim() + const enforcements = splitCommaSeparated(enforcementsRaw) + if (enforcements.length === 0) { + logger.error('At least one KEY:VALUE enforcement is required.') + process.exitCode = 1 + return + } + + const restore = suppressLogs() + let result: SetRoleEnforcementsResult + try { + result = await vault.setRoleEnforcements({ roles, enforcements }) + } finally { + restore() + } + + const table = vault.formatSetRoleEnforcementsResult(result) + logger.info('') + logger.info(vault.renderRoleEnforcementAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(applied=${result.applied}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.applied === 0)) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(roleEnforcementsExample) diff --git a/examples/sdk_example/src/roles/rolePrivileges.ts b/examples/sdk_example/src/roles/rolePrivileges.ts new file mode 100644 index 00000000..941411a2 --- /dev/null +++ b/examples/sdk_example/src/roles/rolePrivileges.ts @@ -0,0 +1,71 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { ChangeRolePrivilegesResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { splitCommaSeparated } from '../utils/format' + +async function rolePrivilegesExample() { + const vault = await login() + + try { + const role = (await prompt('Role name or ID: ')).trim() + if (!role) { + logger.error('A role is required.') + process.exitCode = 1 + return + } + + const node = (await prompt('Managed node name or ID (must already be managed by the role): ')).trim() + if (!node) { + logger.error('A managed node is required.') + process.exitCode = 1 + return + } + + const addRaw = (await prompt('Privileges to add (comma-separated, Enter to skip): ')).trim() + const add = splitCommaSeparated(addRaw) + + const removeRaw = (await prompt('Privileges to remove (comma-separated, Enter to skip): ')).trim() + const remove = splitCommaSeparated(removeRaw) + + if (add.length === 0 && remove.length === 0) { + logger.error('At least one privilege to add or remove is required.') + process.exitCode = 1 + return + } + + const restore = suppressLogs() + let result: ChangeRolePrivilegesResult + try { + result = await vault.changeRolePrivileges({ role, node, add, remove }) + } finally { + restore() + } + + const table = vault.formatChangeRolePrivilegesResult(result) + logger.info('') + logger.info(vault.renderChangeRolePrivilegesAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(succeeded=${result.succeeded}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.succeeded === 0)) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(rolePrivilegesExample) diff --git a/examples/sdk_example/src/roles/roleUsers.ts b/examples/sdk_example/src/roles/roleUsers.ts new file mode 100644 index 00000000..452dc00c --- /dev/null +++ b/examples/sdk_example/src/roles/roleUsers.ts @@ -0,0 +1,100 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { RoleUserResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { splitCommaSeparated } from '../utils/format' + +type VaultHandle = Awaited> + +enum RoleUserMenuChoice { + Add = '1', + Remove = '2', +} + +type OperationInput = { kind: RoleUserMenuChoice; roles: string[]; users: string[] } + +function failWith(message: string): null { + logger.error(message) + process.exitCode = 1 + return null +} + +async function promptList(label: string, message: string): Promise { + const items = splitCommaSeparated((await prompt(message)).trim()) + if (items.length === 0) return failWith(`At least one ${label} is required.`) + return items +} + +async function gatherOperationInput(): Promise { + logger.info('') + logger.info('Select an operation:') + logger.info(` ${RoleUserMenuChoice.Add}) Add users to role(s)`) + logger.info(` ${RoleUserMenuChoice.Remove}) Remove users from role(s)`) + logger.info('') + + const choice = (await prompt(`Operation [${RoleUserMenuChoice.Add}-${RoleUserMenuChoice.Remove}]: `)).trim() + if (choice !== RoleUserMenuChoice.Add && choice !== RoleUserMenuChoice.Remove) { + return failWith(`Invalid choice. Please enter ${RoleUserMenuChoice.Add} or ${RoleUserMenuChoice.Remove}.`) + } + + const roles = await promptList('role name or ID', 'Role name(s) or ID(s) (comma-separated): ') + if (!roles) return null + + const users = await promptList('user email or ID', 'User email(s) or ID(s) (comma-separated): ') + if (!users) return null + + return { kind: choice, roles, users } +} + +async function executeOperation(vault: VaultHandle, input: OperationInput): Promise { + const restore = suppressLogs() + try { + switch (input.kind) { + case RoleUserMenuChoice.Add: + return await vault.addUsersToRoles({ roles: input.roles, users: input.users }) + case RoleUserMenuChoice.Remove: + return await vault.removeUsersFromRoles({ roles: input.roles, users: input.users }) + } + } finally { + restore() + } +} + +function reportResult(vault: VaultHandle, result: RoleUserResult): void { + const table = vault.formatRoleUserResult(result) + logger.info('') + logger.info(vault.renderRoleUserAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(succeeded=${result.succeeded}, skipped=${result.skipped}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.succeeded === 0)) { + process.exitCode = 1 + } +} + +async function roleUsersExample() { + const vault = await login() + try { + const input = await gatherOperationInput() + if (!input) return + + const result = await executeOperation(vault, input) + reportResult(vault, result) + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(roleUsersExample) diff --git a/examples/sdk_example/src/users/update_team_user.ts b/examples/sdk_example/src/users/update_team_user.ts new file mode 100644 index 00000000..01957b1c --- /dev/null +++ b/examples/sdk_example/src/users/update_team_user.ts @@ -0,0 +1,79 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { UpdateUsersOnTeamsResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { splitCommaSeparated } from '../utils/format' + +function parseHideSharedFolders(raw: string): boolean | null { + const value = raw.trim().toLowerCase() + if (value === 'on') return true + if (value === 'off') return false + return null +} + +async function updateTeamUserExample() { + const vault = await login() + + try { + const usersRaw = (await prompt('User email(s) or ID(s) (comma-separated): ')).trim() + const users = splitCommaSeparated(usersRaw) + if (users.length === 0) { + logger.error('At least one user email or ID is required.') + process.exitCode = 1 + return + } + + const teamsRaw = (await prompt('Team name(s) or UID(s) (comma-separated): ')).trim() + const teams = splitCommaSeparated(teamsRaw) + if (teams.length === 0) { + logger.error('At least one team name or UID is required.') + process.exitCode = 1 + return + } + + const hideSharedFolders = parseHideSharedFolders( + await prompt('Hide shared folders for these users on these teams? [on/off]: ') + ) + if (hideSharedFolders === null) { + logger.error('Invalid value. Use on or off.') + process.exitCode = 1 + return + } + + logger.info('Only users that are already members of the given team(s) will be updated.') + + const restore = suppressLogs() + let result: UpdateUsersOnTeamsResult + try { + result = await vault.updateUsersOnTeams({ users, teams, hideSharedFolders }) + } finally { + restore() + } + + const table = vault.formatUpdateTeamUserResult(result) + logger.info('') + logger.info(vault.renderUpdateTeamUserAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(updated=${result.updated}, skipped=${result.skipped}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.updated === 0)) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(updateTeamUserExample)