From cf34c50a57888785dc82efac3f1a79c5741dad32 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Wed, 3 Jun 2026 11:10:27 +0530 Subject: [PATCH 01/14] Roles crud operation implementation --- KeeperSdk/src/index.ts | 77 ++++ KeeperSdk/src/roles/RoleManager.ts | 122 +++++ KeeperSdk/src/roles/addRole.ts | 230 ++++++++++ KeeperSdk/src/roles/deleteRole.ts | 98 ++++ KeeperSdk/src/roles/listRoles.ts | 339 ++++++++++++++ KeeperSdk/src/roles/roleTypes.ts | 50 +++ KeeperSdk/src/roles/roleUtils.ts | 157 +++++++ KeeperSdk/src/roles/updateRole.ts | 168 +++++++ KeeperSdk/src/roles/viewRole.ts | 447 +++++++++++++++++++ KeeperSdk/src/teams/enterpriseData.ts | 83 +++- KeeperSdk/src/utils/constants.ts | 30 +- KeeperSdk/src/utils/index.ts | 1 + KeeperSdk/src/vault/KeeperVault.ts | 38 ++ examples/sdk_example/package.json | 5 + examples/sdk_example/src/roles/addRole.ts | 104 +++++ examples/sdk_example/src/roles/deleteRole.ts | 77 ++++ examples/sdk_example/src/roles/listRoles.ts | 53 +++ examples/sdk_example/src/roles/updateRole.ts | 97 ++++ examples/sdk_example/src/roles/viewRole.ts | 54 +++ keeperapi/src/commands.ts | 30 ++ 20 files changed, 2255 insertions(+), 5 deletions(-) create mode 100644 KeeperSdk/src/roles/RoleManager.ts create mode 100644 KeeperSdk/src/roles/addRole.ts create mode 100644 KeeperSdk/src/roles/deleteRole.ts create mode 100644 KeeperSdk/src/roles/listRoles.ts create mode 100644 KeeperSdk/src/roles/roleTypes.ts create mode 100644 KeeperSdk/src/roles/roleUtils.ts create mode 100644 KeeperSdk/src/roles/updateRole.ts create mode 100644 KeeperSdk/src/roles/viewRole.ts create mode 100644 examples/sdk_example/src/roles/addRole.ts create mode 100644 examples/sdk_example/src/roles/deleteRole.ts create mode 100644 examples/sdk_example/src/roles/listRoles.ts create mode 100644 examples/sdk_example/src/roles/updateRole.ts create mode 100644 examples/sdk_example/src/roles/viewRole.ts diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index b5f2750..6c99d81 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -28,6 +28,7 @@ export { ResultCodes, AuthErrorCode, SessionErrorCode, + RoleErrorCode, TeamErrorCode, UserErrorCode, KEEPER_PUBLIC_HOSTS, @@ -205,6 +206,9 @@ export type { EnterpriseTeamUserLink, EnterpriseRoleUserLink, EnterpriseRoleTeamLink, + EnterpriseRolePrivilegeLink, + EnterpriseRoleManagedNodeLink, + EnterpriseRoleEnforcementLink, EnterpriseQueuedTeamRecord, EnterpriseQueuedTeamUserLink, EnterpriseUserAliasLink, @@ -217,6 +221,79 @@ export type { NodePathOptions, } from './teams/enterpriseData' +export { listRoles, formatRolesTable, renderRolesAsciiTable } from './roles/listRoles' +export { + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + DEFAULT_ROLE_COLUMNS, +} from './roles/roleTypes' +export type { + ListRolesOptions, + ListRoleRow, + RoleColumnInput, + FormattedRolesTable, + FormatRolesTableOptions, +} from './roles/roleTypes' + +export { viewRole, formatRoleView, roleViewTable } from './roles/viewRole' +export type { + RoleView, + RoleTeamInfo, + RoleUserInfo, + RoleManagedNodeInfo, + RoleEnforcementInfo, + FormatRoleViewOptions, + FormattedRoleViewTable, + FormattedManagedNodePrivilegeTable, + RoleViewTableRow, +} from './roles/viewRole' + +export { RoleManager } from './roles/RoleManager' + +export { + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + AddRoleStatus, + AddRoleSkipReason, +} from './roles/addRole' +export type { + AddRoleInput, + AddRoleResult, + AddRoleItemResult, + AddRoleConfirm, + AddRoleConflictPrompt, + FormattedAddRoleTable, +} from './roles/addRole' + +export { + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + UpdateRoleStatus, +} from './roles/updateRole' +export type { + UpdateRoleInput, + UpdateRoleResult, + UpdateRoleItemResult, + FormattedUpdateRoleTable, +} from './roles/updateRole' + +export { + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + DeleteRoleStatus, +} from './roles/deleteRole' +export type { + DeleteRoleInput, + DeleteRoleResult, + DeleteRoleItemResult, + FormattedDeleteRoleTable, +} from './roles/deleteRole' + +export type { RoleToggleInput, RoleToggle, EnforcementPair } from './roles/roleUtils' + export { viewTeam, formatTeamView, teamViewTable } from './teams/viewTeam' export type { TeamView, diff --git a/KeeperSdk/src/roles/RoleManager.ts b/KeeperSdk/src/roles/RoleManager.ts new file mode 100644 index 0000000..1894360 --- /dev/null +++ b/KeeperSdk/src/roles/RoleManager.ts @@ -0,0 +1,122 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { KeeperSdkError, ResultCodes } from '../utils' +import { formatRolesTable, listRoles, renderRolesAsciiTable } from './listRoles' +import type { + FormatRolesTableOptions, + FormattedRolesTable, + ListRoleRow, + ListRolesOptions, +} from './roleTypes' +import { + formatRoleView, + roleViewTable, + viewRole, + type FormatRoleViewOptions, + type FormattedRoleViewTable, + type RoleView, +} from './viewRole' +import { + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + type AddRoleInput, + type AddRoleResult, + type FormattedAddRoleTable, +} from './addRole' +import { + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + type UpdateRoleInput, + type UpdateRoleResult, + type FormattedUpdateRoleTable, +} from './updateRole' +import { + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + type DeleteRoleInput, + type DeleteRoleResult, + type FormattedDeleteRoleTable, +} from './deleteRole' + +export type AuthProvider = () => Auth + +export class RoleManager { + private readonly authProvider: AuthProvider + + constructor(authProvider: AuthProvider) { + this.authProvider = authProvider + } + + public async listRoles(options: ListRolesOptions = {}): Promise { + return listRoles(this.requireAuth(), options) + } + + public formatRolesTable(rows: ListRoleRow[], options: FormatRolesTableOptions = {}): FormattedRolesTable { + return formatRolesTable(rows, options) + } + + public renderRolesAsciiTable( + table: FormattedRolesTable, + options: { minColWidth?: number } = {} + ): string { + return renderRolesAsciiTable(table, options) + } + + public async viewRole(identifier: string): Promise { + return viewRole(this.requireAuth(), identifier) + } + + public formatRoleView(view: RoleView, options: FormatRoleViewOptions = {}): FormattedRoleViewTable { + return formatRoleView(view, options) + } + + public roleViewTable(table: FormattedRoleViewTable): string { + return roleViewTable(table) + } + + public async addRoles(input: AddRoleInput): Promise { + return addRoles(this.requireAuth(), input) + } + + public formatAddRoleResult(result: AddRoleResult): FormattedAddRoleTable { + return formatAddRoleResult(result) + } + + public renderAddRoleAsciiTable(table: FormattedAddRoleTable): string { + return renderAddRoleAsciiTable(table) + } + + public async updateRoles(input: UpdateRoleInput): Promise { + return updateRoles(this.requireAuth(), input) + } + + public formatUpdateRoleResult(result: UpdateRoleResult): FormattedUpdateRoleTable { + return formatUpdateRoleResult(result) + } + + public renderUpdateRoleAsciiTable(table: FormattedUpdateRoleTable): string { + return renderUpdateRoleAsciiTable(table) + } + + public async deleteRoles(input: DeleteRoleInput): Promise { + return deleteRoles(this.requireAuth(), input) + } + + public formatDeleteRoleResult(result: DeleteRoleResult): FormattedDeleteRoleTable { + return formatDeleteRoleResult(result) + } + + public renderDeleteRoleAsciiTable(table: FormattedDeleteRoleTable): string { + return renderDeleteRoleAsciiTable(table) + } + + private requireAuth(): Auth { + const auth = this.authProvider() + if (!auth) { + throw new KeeperSdkError('You are not logged in. Please log in first.', ResultCodes.NOT_LOGGED_IN) + } + return auth + } +} diff --git a/KeeperSdk/src/roles/addRole.ts b/KeeperSdk/src/roles/addRole.ts new file mode 100644 index 0000000..4804110 --- /dev/null +++ b/KeeperSdk/src/roles/addRole.ts @@ -0,0 +1,230 @@ +import { + encryptObjectForStorage, + enterpriseAllocateIdsCommand, + roleAddCommand, + type Auth, + type RoleEditRequest, +} from '@keeper-security/keeperapi' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' +import { EnterpriseDataInclude, EnterpriseDataManager, type EnterpriseRole } from '../teams/enterpriseData' +import { + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveParentNode, +} from '../teams/teamUtils' +import { + applyDecryptedRoleNames, + applyRoleEnforcements, + assertCommandSucceeded, + buildRoleResultRows, + buildRolesByLowerName, + nodePathOrFallback, + normalizeIdentifiers, + parseEnforcements, + renderRoleResultTable, + resolveToggle, + ROLE_TABLE_HEADERS, + validateRoleName, + type RoleToggleInput, +} from './roleUtils' + +const ADD_ROLE_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Nodes, EnterpriseDataInclude.Roles] + +export enum AddRoleStatus { + Created = 'created', + Skipped = 'skipped', + Failed = 'failed', +} + +export enum AddRoleSkipReason { + AlreadyExistsInParent = 'already_exists_in_parent', + ExistsElsewhereDeclined = 'exists_elsewhere_declined', +} + +export type AddRoleConflictPrompt = { + roleName: string + parentNodeId: number + parentNodeName: string + existingRoleId: number + existingRoleName: string + existingNodeId: number +} + +export type AddRoleConfirm = (prompt: AddRoleConflictPrompt) => boolean | Promise + +export type AddRoleInput = { + roles: string[] + parent?: string | number | null + newUser?: RoleToggleInput + visibleBelow?: RoleToggleInput + enforcements?: string[] + force?: boolean + confirm?: AddRoleConfirm +} + +export type AddRoleItemResult = { + roleId: number + roleName: string + nodeId: number + status: AddRoleStatus + skipReason?: AddRoleSkipReason + message?: string +} + +export type AddRoleResult = { + success: boolean + parentNodeId: number + parentNodeName: string + items: AddRoleItemResult[] + created: number + skipped: number + failed: number +} + +export type FormattedAddRoleTable = { + headers: string[] + rows: string[][] + parentNodeName: string + summary: string +} + +export async function addRoles(auth: Auth, input: AddRoleInput): Promise { + const requestedNames = normalizeIdentifiers(input.roles) + if (requestedNames.length === 0) { + throw new KeeperSdkError('No roles to add.', ResultCodes.NO_ROLES_TO_ADD) + } + requestedNames.forEach(validateRoleName) + + const force = input.force === true + const newUserInherit = resolveToggle(input.newUser) ?? false + const visibleBelow = resolveToggle(input.visibleBelow) ?? false + const enforcements = parseEnforcements(input.enforcements ?? []) + + const enterpriseData = new EnterpriseDataManager(auth) + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(ADD_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]) + + const nodes = response.nodes || [] + applyDecryptedNodeNames(nodes, displayNames.nodes) + applyEnterpriseNameToRoot(nodes, response.enterprise_name) + if (parentNeedsNameLookup(input.parent ?? null)) await enterpriseData.decryptNodeNames(nodes) + + const roles = response.roles || [] + applyDecryptedRoleNames(roles, displayNames.roles) + const rolesByLowerName = buildRolesByLowerName(roles) + + const parentNode = resolveParentNode(nodes, input.parent ?? null) + const parentNodeId = parentNode.node_id + const parentNodeName = nodePathOrFallback(nodes, parentNode) + + const treeKey = await enterpriseData.getTreeKey() + if (!treeKey) { + throw new KeeperSdkError( + 'Enterprise tree key is unavailable. The current user may not have permission to administer roles.', + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE + ) + } + + const items: AddRoleItemResult[] = [] + const seenLowerNames = new Set() + for (const name of requestedNames) { + const lower = name.toLowerCase() + if (seenLowerNames.has(lower)) continue + seenLowerNames.add(lower) + + const conflicts = rolesByLowerName.get(lower) || [] + const sameParent = conflicts.find((role) => role.node_id === parentNodeId) + if (sameParent) { + items.push(skippedItem(sameParent.role_id, name, sameParent.node_id, AddRoleSkipReason.AlreadyExistsInParent, + `Role "${name}" already exists in parent node ${parentNodeId}.`)) + continue + } + + if (conflicts.length > 0 && !force) { + const confirmed = await confirmCrossNode(input.confirm, conflicts[0], { roleName: name, parentNodeId, parentNodeName }) + if (!confirmed) { + items.push(skippedItem(conflicts[0].role_id, name, conflicts[0].node_id, AddRoleSkipReason.ExistsElsewhereDeclined, + `Role "${name}" exists in node ${conflicts[0].node_id}; creation declined.`)) + continue + } + } + + try { + const roleId = await allocateRoleId(auth) + const encryptedData = await encryptObjectForStorage({ displayname: name }, treeKey) + await sendRoleAdd(auth, { + role_id: roleId, + node_id: parentNodeId, + encrypted_data: encryptedData, + visible_below: visibleBelow, + new_user_inherit: newUserInherit, + }) + await applyRoleEnforcements(auth, roleId, enforcements) + items.push({ roleId, roleName: name, nodeId: parentNodeId, status: AddRoleStatus.Created }) + } catch (err) { + items.push({ roleId: 0, roleName: name, nodeId: parentNodeId, status: AddRoleStatus.Failed, message: extractErrorMessage(err) }) + } + } + + return finalizeResult(items, parentNodeId, parentNodeName) +} + +export function formatAddRoleResult(result: AddRoleResult): FormattedAddRoleTable { + return { + headers: [...ROLE_TABLE_HEADERS], + rows: buildRoleResultRows(result.items, (item) => item.message || (item.skipReason ?? '')), + parentNodeName: result.parentNodeName, + summary: `Created: ${result.created} Skipped: ${result.skipped} Failed: ${result.failed}`, + } +} + +export function renderAddRoleAsciiTable(table: FormattedAddRoleTable): string { + return renderRoleResultTable(table.headers, table.rows, table.summary, [`Parent: ${table.parentNodeName}`]) +} + +async function allocateRoleId(auth: Auth): Promise { + const response = await auth.executeRestCommand(enterpriseAllocateIdsCommand({ number_requested: 1 })) + if (!response.base_id) { + throw new KeeperSdkError('Failed to allocate enterprise ID for new role.', ResultCodes.ROLE_ID_ALLOCATION_FAILED) + } + return response.base_id +} + +async function sendRoleAdd(auth: Auth, payload: RoleEditRequest): Promise { + const response = await auth.executeRestCommand(roleAddCommand(payload)) + assertCommandSucceeded(response, `role_add failed for role_id=${payload.role_id}`, ResultCodes.ROLE_ADD_FAILED) +} + +async function confirmCrossNode( + confirm: AddRoleConfirm | undefined, + existing: EnterpriseRole, + context: { roleName: string; parentNodeId: number; parentNodeName: string } +): Promise { + if (!confirm) return false + return (await confirm({ + ...context, + existingRoleId: existing.role_id, + existingRoleName: (existing.displayName || '').trim() || String(existing.role_id), + existingNodeId: existing.node_id ?? 0, + })) === true +} + +function skippedItem( + roleId: number, + roleName: string, + nodeId: number, + skipReason: AddRoleSkipReason, + message: string +): AddRoleItemResult { + return { roleId, roleName, nodeId, status: AddRoleStatus.Skipped, skipReason, message } +} + +function finalizeResult(items: AddRoleItemResult[], parentNodeId: number, parentNodeName: string): AddRoleResult { + const created = items.filter((item) => item.status === AddRoleStatus.Created).length + const skipped = items.filter((item) => item.status === AddRoleStatus.Skipped).length + const failed = items.filter((item) => item.status === AddRoleStatus.Failed).length + return { success: failed === 0 && created > 0, parentNodeId, parentNodeName, items, created, skipped, failed } +} diff --git a/KeeperSdk/src/roles/deleteRole.ts b/KeeperSdk/src/roles/deleteRole.ts new file mode 100644 index 0000000..9a893a7 --- /dev/null +++ b/KeeperSdk/src/roles/deleteRole.ts @@ -0,0 +1,98 @@ +import { roleDeleteCommand, type Auth } from '@keeper-security/keeperapi' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' +import { EnterpriseDataInclude, EnterpriseDataManager } from '../teams/enterpriseData' +import { + applyDecryptedRoleNames, + assertCommandSucceeded, + buildRoleResultRows, + normalizeIdentifiers, + renderRoleResultTable, + resolveExistingRoles, + ROLE_TABLE_HEADERS, +} from './roleUtils' + +const DELETE_ROLE_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Roles] + +export enum DeleteRoleStatus { + Deleted = 'deleted', + Failed = 'failed', +} + +export type DeleteRoleInput = { + roles: string[] +} + +export type DeleteRoleItemResult = { + roleId: number + roleName: string + nodeId: number + status: DeleteRoleStatus + message?: string +} + +export type DeleteRoleResult = { + success: boolean + items: DeleteRoleItemResult[] + deleted: number + failed: number +} + +export type FormattedDeleteRoleTable = { + headers: string[] + rows: string[][] + summary: string +} + +export async function deleteRoles(auth: Auth, input: DeleteRoleInput): Promise { + const identifiers = normalizeIdentifiers(input.roles) + if (identifiers.length === 0) { + throw new KeeperSdkError('No roles to delete.', ResultCodes.NO_ROLES_TO_DELETE) + } + + const enterpriseData = new EnterpriseDataManager(auth) + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(DELETE_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]) + + const roles = response.roles || [] + applyDecryptedRoleNames(roles, displayNames.roles) + const resolvedRoles = resolveExistingRoles(roles, identifiers) + + const items: DeleteRoleItemResult[] = [] + for (const role of resolvedRoles) { + const roleName = (role.displayName || '').trim() || String(role.role_id) + const nodeId = role.node_id ?? 0 + try { + await sendRoleDelete(auth, role.role_id) + items.push({ roleId: role.role_id, roleName, nodeId, status: DeleteRoleStatus.Deleted }) + } catch (err) { + items.push({ roleId: role.role_id, roleName, nodeId, status: DeleteRoleStatus.Failed, message: extractErrorMessage(err) }) + } + } + + return finalizeResult(items) +} + +export function formatDeleteRoleResult(result: DeleteRoleResult): FormattedDeleteRoleTable { + return { + headers: [...ROLE_TABLE_HEADERS], + rows: buildRoleResultRows(result.items, (item) => item.message || ''), + summary: `Deleted: ${result.deleted} Failed: ${result.failed}`, + } +} + +export function renderDeleteRoleAsciiTable(table: FormattedDeleteRoleTable): string { + return renderRoleResultTable(table.headers, table.rows, table.summary) +} + +async function sendRoleDelete(auth: Auth, roleId: number): Promise { + const response = await auth.executeRestCommand(roleDeleteCommand({ role_id: roleId })) + assertCommandSucceeded(response, `role_delete failed for role_id=${roleId}`, ResultCodes.ROLE_DELETE_FAILED) +} + +function finalizeResult(items: DeleteRoleItemResult[]): DeleteRoleResult { + const deleted = items.filter((item) => item.status === DeleteRoleStatus.Deleted).length + const failed = items.filter((item) => item.status === DeleteRoleStatus.Failed).length + return { success: failed === 0 && deleted > 0, items, deleted, failed } +} diff --git a/KeeperSdk/src/roles/listRoles.ts b/KeeperSdk/src/roles/listRoles.ts new file mode 100644 index 0000000..e344fff --- /dev/null +++ b/KeeperSdk/src/roles/listRoles.ts @@ -0,0 +1,339 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { isNumber, TOKEN_SEPARATOR_PATTERN } from '../utils' +import { + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseRolePrivilegeLink, + type EnterpriseRoleTeamLink, + type EnterpriseRoleUserLink, + type EnterpriseTeamRecord, + type EnterpriseUser, +} from '../teams/enterpriseData' +import { applyDecryptedNodeNames } from '../teams/teamUtils' +import { + ALL_COLUMNS_WILDCARD, + DEFAULT_ROLE_COLUMNS, + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + type FormatRolesTableOptions, + type FormattedRolesTable, + type ListRoleRow, + type ListRolesOptions, +} from './roleTypes' + +const NODE_PATH_SEPARATOR = '\\' +const MIN_ASCII_COL_WIDTH = 2 + +const HEADER_BY_COLUMN: Record = { + [RoleColumn.VisibleBelow]: 'Visible Below', + [RoleColumn.DefaultRole]: 'Default Role', + [RoleColumn.Admin]: 'Admin', + [RoleColumn.Node]: 'Node', + [RoleColumn.UserCount]: 'User Count', + [RoleColumn.Users]: 'Users', + [RoleColumn.TeamCount]: 'Team Count', + [RoleColumn.Teams]: 'Teams', +} + +type DecorateContext = { + nodePaths: Map + adminRoleIds: Set + roleUsers: Map> + roleTeams: Map> + usernameById: Map + teamNameById: Map +} + +export async function listRoles(auth: Auth, options: ListRolesOptions = {}): Promise { + const columns = resolveColumns(options.columns) + const includes = includesForColumns(columns) + const wantsDisplayNames = + columns.includes(RoleColumn.Node) || + columns.includes(RoleColumn.Teams) || + columns.includes(RoleColumn.TeamCount) + + const enterpriseData = new EnterpriseDataManager(auth) + const emptyDisplayNames: EnterpriseDisplayNames = { nodes: new Map(), roles: new Map() } + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(includes), + wantsDisplayNames ? enterpriseData.getDisplayNames() : Promise.resolve(emptyDisplayNames), + ]) + + const roles = response.roles || [] + const nodes = response.nodes || [] + applyDecryptedNodeNames(nodes, displayNames.nodes) + + for (const role of roles) { + const display = displayNames.roles.get(role.role_id) + if (display) role.displayName = display + } + + const context: DecorateContext = { + nodePaths: buildNodePathLookup(nodes), + adminRoleIds: buildAdminRoleIds(response.role_privileges || []), + roleUsers: buildRoleUserMap(response.role_users || []), + roleTeams: buildRoleTeamMap(response.role_teams || []), + usernameById: buildUsernameMap(response.users || []), + teamNameById: buildTeamNameMap(response.teams || []), + } + + const pattern = options.pattern?.trim() || null + const rows: ListRoleRow[] = [] + for (const role of roles) { + const row = decorateRow(role, columns, context) + if (pattern && !rowMatchesPattern(row, pattern)) continue + rows.push(row) + } + + rows.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) + return rows +} + +export function formatRolesTable( + rows: ListRoleRow[], + options: FormatRolesTableOptions = {} +): FormattedRolesTable { + const columns = resolveColumns(options.columns) + const headers: string[] = ['#', 'Role ID', 'Name', ...columns.map((col) => HEADER_BY_COLUMN[col])] + + const outRows: string[][] = rows.map((row, rowIndex) => { + const cells: string[] = [String(rowIndex + 1), String(row.role_id), row.name] + for (const col of columns) cells.push(formatCell(row, col)) + return cells + }) + + return { headers, rows: outRows } +} + +export function renderRolesAsciiTable( + table: FormattedRolesTable, + options: { minColWidth?: number } = {} +): string { + const { minColWidth = MIN_ASCII_COL_WIDTH } = options + const { headers, rows } = table + const columnCount = headers.length + + const expandedRows: string[][][] = rows.map((row) => + row.map((cell) => (cell.includes('\n') ? cell.split('\n') : [cell])) + ) + + const columnWidths = new Array(columnCount).fill(0) + for (let ci = 0; ci < columnCount; ci += 1) { + columnWidths[ci] = Math.max(headers[ci].length, minColWidth) + } + for (const row of expandedRows) { + for (let ci = 0; ci < columnCount; ci += 1) { + for (const line of row[ci]) { + columnWidths[ci] = Math.max(columnWidths[ci], line.length, minColWidth) + } + } + } + + const padCell = (cell: string, ci: number): string => + cell + ' '.repeat(columnWidths[ci] - cell.length) + const formatPhysicalRow = (cells: string[]): string => + cells.map((cell, ci) => padCell(cell, ci)).join(' ') + + const ruleRow = formatPhysicalRow(columnWidths.map((w) => '-'.repeat(w))) + const lines: string[] = [formatPhysicalRow(headers), ruleRow] + + for (const row of expandedRows) { + const physicalLineCount = Math.max(...row.map((cell) => cell.length)) + for (let li = 0; li < physicalLineCount; li += 1) { + lines.push(formatPhysicalRow(row.map((cell) => cell[li] ?? ''))) + } + } + return lines.join('\n') +} + +function includesForColumns(columns: readonly RoleColumn[]): EnterpriseDataInclude[] { + const set = new Set([EnterpriseDataInclude.Roles]) + for (const col of columns) { + switch (col) { + case RoleColumn.Node: + set.add(EnterpriseDataInclude.Nodes) + break + case RoleColumn.Admin: + set.add(EnterpriseDataInclude.RolePrivileges) + break + case RoleColumn.UserCount: + case RoleColumn.Users: + set.add(EnterpriseDataInclude.RoleUsers) + if (col === RoleColumn.Users) set.add(EnterpriseDataInclude.Users) + break + case RoleColumn.TeamCount: + case RoleColumn.Teams: + set.add(EnterpriseDataInclude.RoleTeams) + if (col === RoleColumn.Teams) set.add(EnterpriseDataInclude.Teams) + break + } + } + return Array.from(set) +} + +function resolveColumns(input: ListRolesOptions['columns']): RoleColumn[] { + if (input == null) return [...DEFAULT_ROLE_COLUMNS] + if (input === ALL_COLUMNS_WILDCARD) return [...SUPPORTED_ROLE_COLUMNS] + + const requested = Array.isArray(input) ? input : input.split(',').map((p) => p.trim()) + const allowed = new Set(SUPPORTED_ROLE_COLUMNS) + const seen = new Set() + for (const col of requested) { + if (col && allowed.has(col)) seen.add(col as RoleColumn) + } + if (seen.size === 0) return [...DEFAULT_ROLE_COLUMNS] + return SUPPORTED_ROLE_COLUMNS.filter((col) => seen.has(col)) +} + +function roleDisplayName(role: EnterpriseRole): string { + return (role.displayName || '').trim() || String(role.role_id) +} + +function tokenize(text: string): string[] { + return text.split(TOKEN_SEPARATOR_PATTERN).filter((t) => t.length > 0) +} + +function rowMatchesPattern(row: ListRoleRow, pattern: string): boolean { + const lower = pattern.toLowerCase() + const tokens: string[] = [String(row.role_id), ...tokenize(row.name.toLowerCase())] + if (row.node) tokens.push(...tokenize(row.node.toLowerCase())) + if (row.user_count != null) tokens.push(String(row.user_count)) + if (row.team_count != null) tokens.push(String(row.team_count)) + for (const list of [row.users, row.teams]) { + if (!list) continue + for (const v of list) tokens.push(...tokenize(v.toLowerCase())) + } + return tokens.some((t) => t.includes(lower)) +} + +function buildNodePathLookup(nodes: EnterpriseNode[]): Map { + return new Map( + nodes.map((node) => [ + node.node_id, + EnterpriseDataManager.getNodePath(nodes, node.node_id, { separator: NODE_PATH_SEPARATOR }), + ]) + ) +} + +function buildAdminRoleIds(privileges: EnterpriseRolePrivilegeLink[]): Set { + const set = new Set() + for (const p of privileges) set.add(p.role_id) + return set +} + +function buildRoleUserMap(links: EnterpriseRoleUserLink[]): Map> { + const map = new Map>() + for (const link of links) { + const set = map.get(link.role_id) + if (set) set.add(link.enterprise_user_id) + else map.set(link.role_id, new Set([link.enterprise_user_id])) + } + return map +} + +function buildRoleTeamMap(links: EnterpriseRoleTeamLink[]): Map> { + const map = new Map>() + for (const link of links) { + const set = map.get(link.role_id) + if (set) set.add(link.team_uid) + else map.set(link.role_id, new Set([link.team_uid])) + } + return map +} + +function buildUsernameMap(users: EnterpriseUser[]): Map { + const map = new Map() + for (const user of users) { + if (isNumber(user.enterprise_user_id) && user.username) { + map.set(user.enterprise_user_id, user.username) + } + } + return map +} + +function buildTeamNameMap(teams: EnterpriseTeamRecord[]): Map { + const map = new Map() + for (const team of teams) { + if (team.team_uid) map.set(team.team_uid, (team.name || team.team_uid).trim()) + } + return map +} + +function resolveSortedNames(ids: Set, nameById: Map): string[] { + const result: string[] = [] + for (const id of ids) { + const name = nameById.get(id) + if (name) result.push(name) + } + return result.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) +} + +function decorateRow( + role: EnterpriseRole, + columns: readonly RoleColumn[], + context: DecorateContext +): ListRoleRow { + const row: ListRoleRow = { role_id: role.role_id, name: roleDisplayName(role) } + + for (const col of columns) { + switch (col) { + case RoleColumn.VisibleBelow: + row.visible_below = role.visible_below ?? false + break + case RoleColumn.DefaultRole: + row.default_role = role.new_user_inherit ?? false + break + case RoleColumn.Admin: + row.admin = context.adminRoleIds.has(role.role_id) + break + case RoleColumn.Node: + row.node = context.nodePaths.get(role.node_id ?? 0) ?? '' + break + case RoleColumn.UserCount: + row.user_count = context.roleUsers.get(role.role_id)?.size ?? 0 + break + case RoleColumn.Users: { + const ids = context.roleUsers.get(role.role_id) + row.users = ids ? resolveSortedNames(ids, context.usernameById) : [] + break + } + case RoleColumn.TeamCount: + row.team_count = context.roleTeams.get(role.role_id)?.size ?? 0 + break + case RoleColumn.Teams: { + const uids = context.roleTeams.get(role.role_id) + row.teams = uids ? resolveSortedNames(uids, context.teamNameById) : [] + break + } + } + } + return row +} + +function formatListCell(values: string[] | undefined): string { + return values?.length ? values.join('\n') : '' +} + +function formatCell(row: ListRoleRow, col: RoleColumn): string { + switch (col) { + case RoleColumn.VisibleBelow: + return row.visible_below == null ? '' : row.visible_below ? 'Yes' : 'No' + case RoleColumn.DefaultRole: + return row.default_role == null ? '' : row.default_role ? 'Yes' : 'No' + case RoleColumn.Admin: + return row.admin == null ? '' : row.admin ? 'Yes' : 'No' + case RoleColumn.Node: + return row.node ?? '' + case RoleColumn.UserCount: + return row.user_count == null ? '' : String(row.user_count) + case RoleColumn.Users: + return formatListCell(row.users) + case RoleColumn.TeamCount: + return row.team_count == null ? '' : String(row.team_count) + case RoleColumn.Teams: + return formatListCell(row.teams) + } +} diff --git a/KeeperSdk/src/roles/roleTypes.ts b/KeeperSdk/src/roles/roleTypes.ts new file mode 100644 index 0000000..a21b86f --- /dev/null +++ b/KeeperSdk/src/roles/roleTypes.ts @@ -0,0 +1,50 @@ +export enum RoleColumn { + VisibleBelow = 'visible_below', + DefaultRole = 'default_role', + Admin = 'admin', + Node = 'node', + UserCount = 'user_count', + Users = 'users', + TeamCount = 'team_count', + Teams = 'teams', +} + +export type RoleColumnInput = RoleColumn | `${RoleColumn}` + +export const SUPPORTED_ROLE_COLUMNS: readonly RoleColumn[] = Object.values(RoleColumn) + +export const DEFAULT_ROLE_COLUMNS: readonly RoleColumn[] = [ + RoleColumn.DefaultRole, + RoleColumn.Admin, + RoleColumn.Node, + RoleColumn.UserCount, +] + +export const ALL_COLUMNS_WILDCARD = '*' as const + +export type ListRolesOptions = { + pattern?: string | null + columns?: RoleColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null +} + +export type ListRoleRow = { + role_id: number + name: string + visible_below?: boolean + default_role?: boolean + admin?: boolean + node?: string + user_count?: number + users?: string[] + team_count?: number + teams?: string[] +} + +export type FormattedRolesTable = { + headers: string[] + rows: string[][] +} + +export type FormatRolesTableOptions = { + columns?: ListRolesOptions['columns'] +} diff --git a/KeeperSdk/src/roles/roleUtils.ts b/KeeperSdk/src/roles/roleUtils.ts new file mode 100644 index 0000000..4a973f7 --- /dev/null +++ b/KeeperSdk/src/roles/roleUtils.ts @@ -0,0 +1,157 @@ +import { roleEnforcementAddCommand, type Auth, type KeeperResponse } from '@keeper-security/keeperapi' +import { KeeperSdkError, ResultCodes } from '../utils' +import { EnterpriseDataManager, type EnterpriseNode, type EnterpriseRole } from '../teams/enterpriseData' + +export const ROLE_TABLE_HEADERS = ['#', 'Status', 'Role Name', 'Role ID', 'Node ID', 'Detail'] as const +export const NODE_PATH_SEPARATOR = '\\' + +export type RoleToggle = 'on' | 'off' +export type RoleToggleInput = RoleToggle | `${RoleToggle}` | null | undefined +export type EnforcementPair = { key: string; value: string } + +type RoleResultRow = { status: string; roleName: string; roleId: number; nodeId: number } + +export function normalizeIdentifiers(values: ReadonlyArray | null | undefined): string[] { + return (values || []) + .map((value) => (typeof value === 'string' ? value.trim() : '')) + .filter((value) => value.length > 0) +} + +export function validateRoleName(name: string): void { + if (!name.trim()) { + throw new KeeperSdkError('Role name cannot be empty.', ResultCodes.ROLE_NAME_EMPTY) + } +} + +export function buildRolesByLowerName(roles: EnterpriseRole[]): Map { + const map = new Map() + for (const role of roles) { + const key = (role.displayName || '').trim().toLowerCase() + if (!key) continue + const bucket = map.get(key) + if (bucket) bucket.push(role) + else map.set(key, [role]) + } + return map +} + +export function resolveExistingRoles(roles: EnterpriseRole[], identifiers: string[]): EnterpriseRole[] { + const byId = new Map(roles.map((role) => [role.role_id, role])) + const byLowerName = buildRolesByLowerName(roles) + + const found = new Map() + const missing: string[] = [] + for (const identifier of identifiers) { + const trimmed = identifier.trim() + const numeric = Number(trimmed) + if (Number.isInteger(numeric) && byId.has(numeric)) { + const match = byId.get(numeric)! + found.set(match.role_id, match) + continue + } + const nameMatches = byLowerName.get(trimmed.toLowerCase()) + if (!nameMatches || nameMatches.length === 0) { + missing.push(trimmed) + continue + } + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Role name "${trimmed}" is not unique. Use Role ID instead.`, + ResultCodes.MULTIPLE_ROLE_MATCHES + ) + } + found.set(nameMatches[0].role_id, nameMatches[0]) + } + + if (missing.length > 0) { + throw new KeeperSdkError( + `Role(s) "${missing.join(', ')}" could not be resolved.`, + ResultCodes.ROLE_NOT_FOUND + ) + } + return Array.from(found.values()) +} + +export function parseEnforcements(rawList: string[]): EnforcementPair[] { + return rawList + .map((raw) => { + const sep = raw.indexOf(':') + if (sep < 1) return null + const key = raw.slice(0, sep).trim() + const value = raw.slice(sep + 1).trim() + return key && value ? ({ key, value } as EnforcementPair) : null + }) + .filter((entry): entry is EnforcementPair => entry !== null) +} + +export function resolveToggle(input: RoleToggleInput): boolean | undefined { + if (input === 'on') return true + if (input === 'off') return false + return undefined +} + +export function applyDecryptedRoleNames(roles: EnterpriseRole[], decrypted: Map): void { + if (decrypted.size === 0) return + for (const role of roles) { + const display = decrypted.get(role.role_id) + if (display) role.displayName = display + } +} + +export function nodePathOrFallback(nodes: EnterpriseNode[], node: EnterpriseNode): string { + const path = EnterpriseDataManager.getNodePath(nodes, node.node_id, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }) + return path || (node.displayName || '').trim() || String(node.node_id) +} + +export function assertCommandSucceeded(response: KeeperResponse, fallbackMessage: string, fallbackCode: string): void { + if ((response.result || '').toLowerCase() === 'fail') { + throw new KeeperSdkError( + response.message || response.result_code || fallbackMessage, + response.result_code || fallbackCode + ) + } +} + +export async function applyRoleEnforcements(auth: Auth, roleId: number, pairs: EnforcementPair[]): Promise { + for (const { key, value } of pairs) { + const response = await auth.executeRestCommand( + roleEnforcementAddCommand({ role_id: roleId, enforcement: key, value }) + ) + assertCommandSucceeded(response, `role_enforcement_add failed: ${key}`, ResultCodes.ROLE_ENFORCEMENT_FAILED) + } +} + +export function buildRoleResultRows( + items: ReadonlyArray, + detailFor: (item: T) => string +): string[][] { + return items.map((item, index) => [ + String(index + 1), + item.status, + item.roleName, + String(item.roleId), + String(item.nodeId), + detailFor(item), + ]) +} + +export function renderRoleResultTable( + headers: ReadonlyArray, + rows: string[][], + summary: string, + prefixLines: ReadonlyArray = [] +): string { + const widths = headers.map((header, i) => Math.max(header.length, ...rows.map((row) => (row[i] || '').length))) + const pad = (cell: string, i: number) => cell + ' '.repeat(Math.max(0, widths[i] - cell.length)) + const formatRow = (cells: ReadonlyArray) => cells.map((cell, i) => pad(cell, i)).join(' ') + return [ + ...prefixLines, + formatRow(headers), + formatRow(widths.map((width) => '-'.repeat(width))), + ...rows.map(formatRow), + summary, + ].join('\n') +} diff --git a/KeeperSdk/src/roles/updateRole.ts b/KeeperSdk/src/roles/updateRole.ts new file mode 100644 index 0000000..a0bee3f --- /dev/null +++ b/KeeperSdk/src/roles/updateRole.ts @@ -0,0 +1,168 @@ +import { + encryptObjectForStorage, + roleUpdateCommand, + type Auth, + type RoleEditRequest, +} from '@keeper-security/keeperapi' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../utils' +import { EnterpriseDataInclude, EnterpriseDataManager, type EnterpriseRole } from '../teams/enterpriseData' +import { + applyDecryptedNodeNames, + applyEnterpriseNameToRoot, + parentNeedsNameLookup, + resolveParentNode, +} from '../teams/teamUtils' +import { + applyDecryptedRoleNames, + applyRoleEnforcements, + assertCommandSucceeded, + buildRoleResultRows, + normalizeIdentifiers, + parseEnforcements, + renderRoleResultTable, + resolveExistingRoles, + resolveToggle, + ROLE_TABLE_HEADERS, + type RoleToggleInput, +} from './roleUtils' + +const UPDATE_ROLE_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Nodes, EnterpriseDataInclude.Roles] + +export enum UpdateRoleStatus { + Updated = 'updated', + Failed = 'failed', +} + +export type UpdateRoleInput = { + roles: string[] + name?: string + parent?: string | number | null + newUser?: RoleToggleInput + visibleBelow?: RoleToggleInput + enforcements?: string[] +} + +export type UpdateRoleItemResult = { + roleId: number + roleName: string + nodeId: number + status: UpdateRoleStatus + message?: string +} + +export type UpdateRoleResult = { + success: boolean + items: UpdateRoleItemResult[] + updated: number + failed: number +} + +export type FormattedUpdateRoleTable = { + headers: string[] + rows: string[][] + summary: string +} + +export async function updateRoles(auth: Auth, input: UpdateRoleInput): Promise { + const identifiers = normalizeIdentifiers(input.roles) + if (identifiers.length === 0) { + throw new KeeperSdkError('No roles to update.', ResultCodes.NO_ROLES_TO_UPDATE) + } + + const newName = input.name?.trim() || undefined + if (newName && identifiers.length > 1) { + throw new KeeperSdkError( + 'Cannot rename more than one role in a single update.', + ResultCodes.ROLE_RENAME_MULTI_NOT_ALLOWED + ) + } + + const newUserInherit = resolveToggle(input.newUser) + const visibleBelow = resolveToggle(input.visibleBelow) + const enforcements = parseEnforcements(input.enforcements ?? []) + + const enterpriseData = new EnterpriseDataManager(auth) + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(UPDATE_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]) + + const nodes = response.nodes || [] + applyDecryptedNodeNames(nodes, displayNames.nodes) + applyEnterpriseNameToRoot(nodes, response.enterprise_name) + if (parentNeedsNameLookup(input.parent ?? null)) await enterpriseData.decryptNodeNames(nodes) + + const roles = response.roles || [] + applyDecryptedRoleNames(roles, displayNames.roles) + const resolvedRoles = resolveExistingRoles(roles, identifiers) + + let overrideNodeId: number | null = null + if (input.parent !== undefined && input.parent !== null && input.parent !== '') { + overrideNodeId = resolveParentNode(nodes, input.parent).node_id + } + + const needsTreeKey = Boolean(newName) || resolvedRoles.some((role) => !role.encrypted_data) + const treeKey = needsTreeKey ? await enterpriseData.getTreeKey() : null + + const items: UpdateRoleItemResult[] = [] + for (const role of resolvedRoles) { + const targetNodeId = overrideNodeId ?? role.node_id ?? 0 + const currentName = (role.displayName || '').trim() || String(role.role_id) + + try { + const payload: RoleEditRequest = { + role_id: role.role_id, + node_id: targetNodeId, + encrypted_data: await resolveEncryptedData(role, newName, currentName, treeKey), + visible_below: visibleBelow ?? role.visible_below ?? false, + new_user_inherit: newUserInherit ?? role.new_user_inherit ?? false, + } + await sendRoleUpdate(auth, payload) + await applyRoleEnforcements(auth, role.role_id, enforcements) + items.push({ roleId: role.role_id, roleName: newName || currentName, nodeId: targetNodeId, status: UpdateRoleStatus.Updated }) + } catch (err) { + items.push({ roleId: role.role_id, roleName: currentName, nodeId: role.node_id ?? 0, status: UpdateRoleStatus.Failed, message: extractErrorMessage(err) }) + } + } + + return finalizeResult(items) +} + +export function formatUpdateRoleResult(result: UpdateRoleResult): FormattedUpdateRoleTable { + return { + headers: [...ROLE_TABLE_HEADERS], + rows: buildRoleResultRows(result.items, (item) => item.message || ''), + summary: `Updated: ${result.updated} Failed: ${result.failed}`, + } +} + +export function renderUpdateRoleAsciiTable(table: FormattedUpdateRoleTable): string { + return renderRoleResultTable(table.headers, table.rows, table.summary) +} + +async function resolveEncryptedData( + role: EnterpriseRole, + newName: string | undefined, + currentName: string, + treeKey: Uint8Array | null +): Promise { + if (!newName && role.encrypted_data) return role.encrypted_data + if (!treeKey) { + throw new KeeperSdkError( + `Enterprise tree key is unavailable; cannot ${newName ? 'rename role' : 'preserve role name'}.`, + ResultCodes.ENTERPRISE_TREE_KEY_UNAVAILABLE + ) + } + return encryptObjectForStorage({ displayname: newName || currentName }, treeKey) +} + +async function sendRoleUpdate(auth: Auth, payload: RoleEditRequest): Promise { + const response = await auth.executeRestCommand(roleUpdateCommand(payload)) + assertCommandSucceeded(response, `role_update failed for role_id=${payload.role_id}`, ResultCodes.ROLE_UPDATE_FAILED) +} + +function finalizeResult(items: UpdateRoleItemResult[]): UpdateRoleResult { + const updated = items.filter((item) => item.status === UpdateRoleStatus.Updated).length + const failed = items.filter((item) => item.status === UpdateRoleStatus.Failed).length + return { success: failed === 0 && updated > 0, items, updated, failed } +} diff --git a/KeeperSdk/src/roles/viewRole.ts b/KeeperSdk/src/roles/viewRole.ts new file mode 100644 index 0000000..b90e4a4 --- /dev/null +++ b/KeeperSdk/src/roles/viewRole.ts @@ -0,0 +1,447 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { KeeperSdkError, ResultCodes } from '../utils' +import { + EnterpriseDataInclude, + EnterpriseDataManager, + type EnterpriseDisplayNames, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseRoleEnforcementLink, + type EnterpriseRoleManagedNodeLink, + type EnterpriseRolePrivilegeLink, + type EnterpriseRoleTeamLink, + type EnterpriseRoleUserLink, + type EnterpriseTeamRecord, + type EnterpriseUser, +} from '../teams/enterpriseData' + +const NODE_PATH_SEPARATOR = '\\' + +const VIEW_ROLE_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Roles, + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Teams, + EnterpriseDataInclude.Users, + EnterpriseDataInclude.RoleUsers, + EnterpriseDataInclude.RoleTeams, + EnterpriseDataInclude.RolePrivileges, + EnterpriseDataInclude.ManagedNodes, + EnterpriseDataInclude.RoleEnforcements, +] + +export type RoleTeamInfo = { + team_uid: string + team_name: string +} + +export type RoleUserInfo = { + enterprise_user_id: number + username: string +} + +export type RoleManagedNodeInfo = { + node_id: number + node_name: string + cascade: boolean + privileges: string[] +} + +export type RoleEnforcementInfo = { + name: string + value: string +} + +export type RoleView = { + role_id: number + role_name: string + node_id: number + node_name: string + default_role: boolean + visible_below: boolean + role_teams?: RoleTeamInfo[] + role_users?: RoleUserInfo[] + managed_nodes?: RoleManagedNodeInfo[] + role_enforcements: RoleEnforcementInfo[] +} + +export type FormatRoleViewOptions = { + verbose?: boolean +} + +export type RoleViewTableRow = { + label: string + value: string | string[] + id?: number | number[] +} + +export type FormattedRoleViewTable = { + mainRows: RoleViewTableRow[] + enforcementRows: RoleEnforcementInfo[] + managedNodeTable: FormattedManagedNodePrivilegeTable | null + hasIdColumn: boolean +} + +export type FormattedManagedNodePrivilegeTable = { + nodeHeaders: string[] + privilegeRows: Array<{ privilege: string; granted: boolean[] }> + cascadeRow: boolean[] +} + +export async function viewRole(auth: Auth, identifier: string): Promise { + const enterpriseData = new EnterpriseDataManager(auth) + const [response, displayNames] = await Promise.all([ + enterpriseData.getData(VIEW_ROLE_INCLUDES), + enterpriseData.getDisplayNames(), + ]) + + const role = resolveRole(response.roles || [], displayNames.roles, identifier) + const nodePath = resolveNodePath(response.nodes || [], displayNames, role.node_id ?? 0) + + const roleTeams = buildRoleTeams(response.role_teams || [], response.teams || [], role.role_id) + const roleUsers = buildRoleUsers(response.role_users || [], response.users || [], role.role_id) + const managedNodes = buildManagedNodes( + response.managed_nodes || [], + response.role_privileges || [], + response.nodes || [], + displayNames, + role.role_id, + response.enterprise_name + ) + const enforcements = buildEnforcements(response.role_enforcements || [], role.role_id) + + const view: RoleView = { + role_id: role.role_id, + role_name: (role.displayName || '').trim() || String(role.role_id), + node_id: role.node_id ?? 0, + node_name: nodePath, + default_role: role.new_user_inherit ?? false, + visible_below: role.visible_below ?? false, + role_enforcements: enforcements, + } + if (roleTeams.length > 0) view.role_teams = roleTeams + if (roleUsers.length > 0) view.role_users = roleUsers + if (managedNodes.length > 0) view.managed_nodes = managedNodes + + return view +} + +export function formatRoleView( + view: RoleView, + options: FormatRoleViewOptions = {} +): FormattedRoleViewTable { + const verbose = options.verbose === true + + const mainRows: RoleViewTableRow[] = [ + { label: 'Role ID', value: String(view.role_id) }, + { label: 'Role Name', value: view.role_name }, + { + label: 'Node Name', + value: view.node_name, + id: verbose ? view.node_id : undefined, + }, + { label: 'Default Role', value: boolText(view.default_role) }, + { label: 'Visible Below', value: boolText(view.visible_below) }, + ] + + if (view.role_users && view.role_users.length > 0) { + mainRows.push({ + label: 'User(s)', + value: view.role_users.map((u) => u.username), + id: verbose ? view.role_users.map((u) => u.enterprise_user_id) : undefined, + }) + } + + if (view.role_teams && view.role_teams.length > 0) { + mainRows.push({ + label: 'Team(s)', + value: view.role_teams.map((t) => t.team_name), + }) + } + + const managedNodeTable = buildFormattedManagedNodeTable(view.managed_nodes ?? []) + + return { + mainRows, + enforcementRows: view.role_enforcements, + managedNodeTable, + hasIdColumn: verbose, + } +} + +export function roleViewTable(table: FormattedRoleViewTable): string { + const sections: string[] = [] + + sections.push(renderMainSection(table)) + + sections.push(renderEnforcementsSection(table.enforcementRows)) + + if (table.managedNodeTable) { + sections.push(renderManagedNodesSection(table.managedNodeTable)) + } + + return sections.join('\n\n') +} + +function resolveRole( + roles: EnterpriseRole[], + decryptedNames: Map, + identifier: string +): EnterpriseRole { + const trimmed = (identifier ?? '').trim() + if (!trimmed) { + throw new KeeperSdkError('Role name or ID is required.', ResultCodes.ROLE_REQUIRED) + } + + for (const role of roles) { + const display = decryptedNames.get(role.role_id) + if (display) role.displayName = display + } + + const numeric = Number(trimmed) + if (Number.isFinite(numeric) && Number.isInteger(numeric)) { + const byId = roles.find((r) => r.role_id === numeric) + if (byId) return byId + } + + const lowered = trimmed.toLowerCase() + const nameMatches = roles.filter( + (r) => (r.displayName || '').trim().toLowerCase() === lowered + ) + if (nameMatches.length === 1) return nameMatches[0] + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Multiple roles match name "${trimmed}". Specify the role ID instead.`, + ResultCodes.MULTIPLE_ROLE_MATCHES + ) + } + throw new KeeperSdkError(`Role "${trimmed}" does not exist.`, ResultCodes.ROLE_NOT_FOUND) +} + +function resolveNodePath( + nodes: EnterpriseNode[], + displayNames: EnterpriseDisplayNames, + nodeId: number +): string { + for (const node of nodes) { + const display = displayNames.nodes.get(node.node_id) + if (display) node.displayName = display + } + return EnterpriseDataManager.getNodePath(nodes, nodeId, { + omitRoot: false, + separator: NODE_PATH_SEPARATOR, + }) +} + +function buildRoleTeams( + roleTeamLinks: EnterpriseRoleTeamLink[], + teams: EnterpriseTeamRecord[], + roleId: number +): RoleTeamInfo[] { + const teamByUid = new Map() + for (const team of teams) teamByUid.set(team.team_uid, team) + + const result: RoleTeamInfo[] = [] + for (const link of roleTeamLinks) { + if (link.role_id !== roleId) continue + const team = teamByUid.get(link.team_uid) + if (team) result.push({ team_uid: team.team_uid, team_name: (team.name || team.team_uid).trim() }) + } + result.sort((a, b) => a.team_name.localeCompare(b.team_name, undefined, { sensitivity: 'base' })) + return result +} + +function buildRoleUsers( + roleUserLinks: EnterpriseRoleUserLink[], + users: EnterpriseUser[], + roleId: number +): RoleUserInfo[] { + const userById = new Map() + for (const user of users) userById.set(user.enterprise_user_id, user) + + const result: RoleUserInfo[] = [] + for (const link of roleUserLinks) { + if (link.role_id !== roleId) continue + const user = userById.get(link.enterprise_user_id) + if (user?.username) { + result.push({ enterprise_user_id: link.enterprise_user_id, username: user.username }) + } + } + result.sort((a, b) => a.username.localeCompare(b.username, undefined, { sensitivity: 'base' })) + return result +} + +function buildManagedNodes( + managedNodeLinks: EnterpriseRoleManagedNodeLink[], + privileges: EnterpriseRolePrivilegeLink[], + nodes: EnterpriseNode[], + displayNames: EnterpriseDisplayNames, + roleId: number, + enterpriseName?: string +): RoleManagedNodeInfo[] { + const nodeById = new Map() + for (const node of nodes) { + const display = displayNames.nodes.get(node.node_id) + if (display) node.displayName = display + nodeById.set(node.node_id, node) + } + + const privilegesByNode = new Map() + for (const p of privileges) { + if (p.role_id !== roleId || !p.privilege_type) continue + const list = privilegesByNode.get(p.managed_node_id) + if (list) list.push(p.privilege_type) + else privilegesByNode.set(p.managed_node_id, [p.privilege_type]) + } + + const result: RoleManagedNodeInfo[] = [] + for (const link of managedNodeLinks) { + if (link.role_id !== roleId) continue + const node = nodeById.get(link.managed_node_id) + let nodeName = (node?.displayName || '').trim() + if (!nodeName && node && !node.parent_id && enterpriseName) nodeName = enterpriseName + if (!nodeName) nodeName = String(link.managed_node_id) + const nodePrivileges = (privilegesByNode.get(link.managed_node_id) ?? []).sort() + result.push({ + node_id: link.managed_node_id, + node_name: nodeName, + cascade: link.cascade_node_management, + privileges: nodePrivileges, + }) + } + result.sort((a, b) => a.node_name.localeCompare(b.node_name, undefined, { sensitivity: 'base' })) + return result +} + +function buildEnforcements( + links: EnterpriseRoleEnforcementLink[], + roleId: number +): RoleEnforcementInfo[] { + const result: RoleEnforcementInfo[] = [] + for (const link of links) { + if (link.role_id !== roleId || !link.value) continue + result.push({ name: link.enforcement_type, value: link.value }) + } + result.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) + return result +} + +function buildFormattedManagedNodeTable( + managedNodes: RoleManagedNodeInfo[] +): FormattedManagedNodePrivilegeTable | null { + if (managedNodes.length === 0) return null + + const allPrivileges = new Set() + for (const mn of managedNodes) { + for (const p of mn.privileges) allPrivileges.add(p) + } + + const privileges = [...allPrivileges].sort() + const privilegeRows = privileges.map((privilege) => ({ + privilege, + granted: managedNodes.map((mn) => mn.privileges.includes(privilege)), + })) + + return { + nodeHeaders: managedNodes.map((mn) => mn.node_name), + privilegeRows, + cascadeRow: managedNodes.map((mn) => mn.cascade), + } +} + +function renderMainSection(table: FormattedRoleViewTable): string { + const { mainRows, hasIdColumn } = table + + const labelWidth = mainRows.reduce((max, row) => Math.max(max, row.label.length), 0) + const expandedRows = mainRows.map((row) => ({ + label: row.label, + valueLines: asLines(row.value), + idLines: hasIdColumn ? asIdLines(row.id) : [], + })) + + const valueWidth = expandedRows.reduce( + (max, row) => Math.max(max, ...row.valueLines.map((l) => l.length)), + 0 + ) + const idWidth = hasIdColumn + ? expandedRows.reduce((max, row) => Math.max(max, ...row.idLines.map((l) => l.length)), 0) + : 0 + + const padRight = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length)) + const padLeft = (text: string, width: number) => ' '.repeat(Math.max(0, width - text.length)) + text + + const lines: string[] = [] + for (const row of expandedRows) { + const lineCount = Math.max(row.valueLines.length, hasIdColumn ? row.idLines.length : 0, 1) + for (let li = 0; li < lineCount; li++) { + const labelCell = padLeft(li === 0 ? row.label : '', labelWidth) + const valueCell = padRight(row.valueLines[li] ?? '', valueWidth) + const cells: string[] = [labelCell, valueCell] + if (hasIdColumn) cells.push(padRight(row.idLines[li] ?? '', idWidth)) + lines.push(cells.join(' ').trimEnd()) + } + } + return lines.join('\n') +} + +function renderEnforcementsSection(rows: RoleEnforcementInfo[]): string { + const title = 'Role Enforcements' + if (rows.length === 0) return `${title}\n (none)` + + const nameWidth = Math.max('Name'.length, ...rows.map((r) => r.name.length)) + const valWidth = Math.max('Value'.length, ...rows.map((r) => r.value.length)) + + const pad = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length)) + const rule = `${'-'.repeat(nameWidth)} ${'-'.repeat(valWidth)}` + const lines: string[] = [ + title, + `${pad('Name', nameWidth)} ${'Value'}`, + rule, + ...rows.map((r) => `${pad(r.name, nameWidth)} ${r.value}`), + ] + return lines.join('\n') +} + +function renderManagedNodesSection(table: FormattedManagedNodePrivilegeTable): string { + const title = 'Managed Node Privileges' + const colHeaders = ['Privilege', ...table.nodeHeaders] + + const colWidths = colHeaders.map((h) => h.length) + for (const row of table.privilegeRows) { + colWidths[0] = Math.max(colWidths[0], row.privilege.length) + row.granted.forEach((_, i) => { + colWidths[i + 1] = Math.max(colWidths[i + 1], 1) + }) + } + colWidths[0] = Math.max(colWidths[0], 'Cascade Node Permissions'.length) + + const pad = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length)) + const formatRow = (cells: string[]) => cells.map((c, i) => pad(c, colWidths[i])).join(' ').trimEnd() + + const rule = formatRow(colWidths.map((w) => '-'.repeat(w))) + const lines: string[] = [ + title, + formatRow(colHeaders), + rule, + ...table.privilegeRows.map((row) => + formatRow([row.privilege, ...row.granted.map((g) => (g ? 'X' : ''))]) + ), + rule, + formatRow(['Cascade Node Permissions', ...table.cascadeRow.map((c) => (c ? 'X' : ''))]), + ] + return lines.join('\n') +} + +function boolText(value: boolean): string { + return value ? 'True' : 'False' +} + +function asLines(value: string | string[]): string[] { + if (Array.isArray(value)) return value.length > 0 ? value : [''] + return [value] +} + +function asIdLines(id: number | number[] | undefined): string[] { + if (id == null) return [] + if (Array.isArray(id)) return id.length > 0 ? id.map(String) : [''] + return [String(id)] +} \ No newline at end of file diff --git a/KeeperSdk/src/teams/enterpriseData.ts b/KeeperSdk/src/teams/enterpriseData.ts index 2fb55fd..d933621 100644 --- a/KeeperSdk/src/teams/enterpriseData.ts +++ b/KeeperSdk/src/teams/enterpriseData.ts @@ -20,6 +20,9 @@ export enum EnterpriseDataInclude { Roles = 'roles', RoleUsers = 'role_users', RoleTeams = 'role_teams', + RolePrivileges = 'role_privileges', + ManagedNodes = 'managed_nodes', + RoleEnforcements = 'role_enforcements', Teams = 'teams', TeamUsers = 'team_users', QueuedTeams = 'queued_teams', @@ -33,6 +36,9 @@ const INCLUDE_TO_ENTITY: Record { const host = this.config.host const baseDeviceConfig = await this.sessionManager.getDeviceConfig(host) @@ -471,6 +483,32 @@ export class KeeperVault { return result } + public async listRoles(options?: ListRolesOptions): Promise { + return this.roleManager.listRoles(options ?? {}) + } + + public async viewRole(identifier: string): Promise { + return this.roleManager.viewRole(identifier) + } + + public async addRoles(input: AddRoleInput): Promise { + const result = await this.roleManager.addRoles(input) + if (result.created > 0) await this.syncIfNeeded() + return result + } + + public async updateRoles(input: UpdateRoleInput): Promise { + const result = await this.roleManager.updateRoles(input) + if (result.updated > 0) await this.syncIfNeeded() + return result + } + + public async deleteRoles(input: DeleteRoleInput): Promise { + const result = await this.roleManager.deleteRoles(input) + if (result.deleted > 0) await this.syncIfNeeded() + return result + } + public async changeDirectory(path: string): Promise { return this.folderManager.changeDirectory(path) } diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index 9346383..f53fc68 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -24,6 +24,11 @@ "folders:tree": "ts-node src/folders/tree.ts", "shared-folders:list-sf": "ts-node src/sharedFolders/list_sf.ts", "shared-folders:share-folder": "ts-node src/sharedFolders/share_folder.ts", + "roles:list": "ts-node src/roles/listRoles.ts", + "roles:view": "ts-node src/roles/viewRole.ts", + "roles:add": "ts-node src/roles/addRole.ts", + "roles:update": "ts-node src/roles/updateRole.ts", + "roles:delete": "ts-node src/roles/deleteRole.ts", "teams:list": "ts-node src/teams/list_teams.ts", "teams:view": "ts-node src/teams/view_team.ts", "teams:add": "ts-node src/teams/add_team.ts", diff --git a/examples/sdk_example/src/roles/addRole.ts b/examples/sdk_example/src/roles/addRole.ts new file mode 100644 index 0000000..1985db2 --- /dev/null +++ b/examples/sdk_example/src/roles/addRole.ts @@ -0,0 +1,104 @@ +import { + cleanup, + extractErrorMessage, + formatAddRoleResult, + login, + logger, + prompt, + renderAddRoleAsciiTable, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { + AddRoleConfirm, + AddRoleResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +function parseRoleNames(raw: string): string[] { + const seen = new Set() + const out: string[] = [] + for (const token of raw.split(',')) { + const trimmed = token.trim() + if (!trimmed) continue + const key = trimmed.toLowerCase() + if (seen.has(key)) continue + seen.add(key) + out.push(trimmed) + } + return out +} + +async function addRoleExample() { + const vault = await login() + + try { + const rolesRaw = (await prompt('Role name(s) to create (comma-separated): ')).trim() + const roles = parseRoleNames(rolesRaw) + if (roles.length === 0) { + logger.error('At least one role name is required.') + process.exitCode = 1 + return + } + + const parentRaw = (await prompt('Parent node name or ID (Enter for enterprise root): ')).trim() + const parent: string | null = parentRaw || null + + const newUserRaw = (await prompt('Assign to new users? [on/off, Enter to skip]: ')).trim().toLowerCase() + const newUser = newUserRaw === 'on' ? 'on' : newUserRaw === 'off' ? 'off' : null + + const visibleBelowRaw = (await prompt('Visible below parent node? [on/off, Enter to skip]: ')).trim().toLowerCase() + const visibleBelow = visibleBelowRaw === 'on' ? 'on' : visibleBelowRaw === 'off' ? 'off' : null + + const enforcementsRaw = (await prompt('Enforcements (KEY:VALUE, comma-separated, Enter to skip): ')).trim() + const enforcements = enforcementsRaw + ? enforcementsRaw.split(',').map((s) => s.trim()).filter(Boolean) + : [] + + const force = isYes(await prompt('Force-create on cross-node duplicates without prompting? [y/N]: ')) + + const confirm: AddRoleConfirm = async ({ roleName, existingRoleId, existingNodeId, parentNodeName }) => { + const ans = await prompt( + `Role "${roleName}" already exists in node ${existingNodeId} (id=${existingRoleId}). ` + + `Create another in "${parentNodeName}"? [y/N]: ` + ) + return isYes(ans) + } + + const restore = suppressLogs() + let result: AddRoleResult + try { + result = await vault.addRoles({ + roles, + parent, + newUser: newUser as 'on' | 'off' | null, + visibleBelow: visibleBelow as 'on' | 'off' | null, + enforcements, + force, + confirm: force ? undefined : confirm, + }) + } finally { + restore() + } + + const table = formatAddRoleResult(result) + logger.info('') + logger.info(renderAddRoleAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(created=${result.created}, skipped=${result.skipped}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.created === 0)) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(addRoleExample) diff --git a/examples/sdk_example/src/roles/deleteRole.ts b/examples/sdk_example/src/roles/deleteRole.ts new file mode 100644 index 0000000..d2920ac --- /dev/null +++ b/examples/sdk_example/src/roles/deleteRole.ts @@ -0,0 +1,77 @@ +import { + cleanup, + extractErrorMessage, + formatDeleteRoleResult, + login, + logger, + prompt, + renderDeleteRoleAsciiTable, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { DeleteRoleResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +function parseRoleIdentifiers(raw: string): string[] { + const seen = new Set() + const out: string[] = [] + for (const token of raw.split(',')) { + const trimmed = token.trim() + if (!trimmed) continue + const key = trimmed.toLowerCase() + if (seen.has(key)) continue + seen.add(key) + out.push(trimmed) + } + return out +} + +async function deleteRoleExample() { + const vault = await login() + + try { + const rolesRaw = (await prompt('Role name(s) or ID(s) to delete (comma-separated): ')).trim() + const roles = parseRoleIdentifiers(rolesRaw) + if (roles.length === 0) { + logger.error('At least one role name or ID is required.') + process.exitCode = 1 + return + } + + const confirmed = isYes( + await prompt(`Permanently delete ${roles.length} role(s)? This cannot be undone. [y/N]: `) + ) + if (!confirmed) { + logger.info('Deletion cancelled.') + return + } + + const restore = suppressLogs() + let result: DeleteRoleResult + try { + result = await vault.deleteRoles({ roles }) + } finally { + restore() + } + + const table = formatDeleteRoleResult(result) + logger.info('') + logger.info(renderDeleteRoleAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(deleted=${result.deleted}, failed=${result.failed})` + ) + + if (result.failed > 0 || (!result.success && result.deleted === 0)) { + process.exitCode = 1 + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(deleteRoleExample) diff --git a/examples/sdk_example/src/roles/listRoles.ts b/examples/sdk_example/src/roles/listRoles.ts new file mode 100644 index 0000000..3e5c7e7 --- /dev/null +++ b/examples/sdk_example/src/roles/listRoles.ts @@ -0,0 +1,53 @@ +import { + cleanup, + extractErrorMessage, + formatRolesTable, + login, + logger, + prompt, + renderRolesAsciiTable, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { ListRoleRow } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +async function listRolesExample() { + const vault = await login() + + try { + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + const pattern = (await prompt('Search pattern (Enter to skip): ')).trim() || null + + let rows: ListRoleRow[] + const restore = suppressLogs() + try { + rows = await vault.listRoles({ pattern }) + } finally { + restore() + } + + if (rows.length === 0) { + logger.info(pattern ? `No roles matched "${pattern}".` : 'No roles found in enterprise.') + return + } + + if (asJson) { + logger.info(JSON.stringify(rows, null, 2)) + return + } + + const table = formatRolesTable(rows) + logger.info('') + logger.info(renderRolesAsciiTable(table)) + logger.info('') + logger.info(`Total: ${rows.length} role${rows.length === 1 ? '' : 's'}`) + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listRolesExample) diff --git a/examples/sdk_example/src/roles/updateRole.ts b/examples/sdk_example/src/roles/updateRole.ts new file mode 100644 index 0000000..8b62ccd --- /dev/null +++ b/examples/sdk_example/src/roles/updateRole.ts @@ -0,0 +1,97 @@ +import { + cleanup, + extractErrorMessage, + formatUpdateRoleResult, + login, + logger, + prompt, + renderUpdateRoleAsciiTable, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { UpdateRoleResult } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' + +function parseRoleIdentifiers(raw: string): string[] { + const seen = new Set() + const out: string[] = [] + for (const token of raw.split(',')) { + const trimmed = token.trim() + if (!trimmed) continue + const key = trimmed.toLowerCase() + if (seen.has(key)) continue + seen.add(key) + out.push(trimmed) + } + return out +} + +async function updateRoleExample() { + const vault = await login() + + try { + const rolesRaw = (await prompt('Role name(s) or ID(s) to update (comma-separated): ')).trim() + const roles = parseRoleIdentifiers(rolesRaw) + if (roles.length === 0) { + logger.error('At least one role name or ID is required.') + process.exitCode = 1 + return + } + + let name: string | undefined + if (roles.length === 1) { + const renamed = (await prompt('New display name (Enter to keep current): ')).trim() + name = renamed || undefined + } else { + logger.info('Renaming is only allowed when updating a single role; skipping rename prompt.') + } + + const parentRaw = (await prompt('New parent node name or ID (Enter to keep current): ')).trim() + const parent: string | null = parentRaw || null + + const newUserRaw = (await prompt('Assign to new users? [on/off, Enter to keep current]: ')).trim().toLowerCase() + const newUser = newUserRaw === 'on' ? 'on' : newUserRaw === 'off' ? 'off' : null + + const visibleBelowRaw = (await prompt('Visible below? [on/off, Enter to keep current]: ')).trim().toLowerCase() + const visibleBelow = visibleBelowRaw === 'on' ? 'on' : visibleBelowRaw === 'off' ? 'off' : null + + const enforcementsRaw = (await prompt('Enforcements to set (KEY:VALUE, comma-separated, Enter to skip): ')).trim() + const enforcements = enforcementsRaw + ? enforcementsRaw.split(',').map((s) => s.trim()).filter(Boolean) + : [] + + const restore = suppressLogs() + let result: UpdateRoleResult + try { + result = await vault.updateRoles({ + roles, + name, + parent, + newUser: newUser as 'on' | 'off' | null, + visibleBelow: visibleBelow as 'on' | 'off' | null, + enforcements, + }) + } finally { + restore() + } + + const table = formatUpdateRoleResult(result) + logger.info('') + logger.info(renderUpdateRoleAsciiTable(table)) + logger.info('') + logger.info( + `Result: ${result.success ? 'success' : 'partial/failed'} ` + + `(updated=${result.updated}, 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(updateRoleExample) diff --git a/examples/sdk_example/src/roles/viewRole.ts b/examples/sdk_example/src/roles/viewRole.ts new file mode 100644 index 0000000..b4788f3 --- /dev/null +++ b/examples/sdk_example/src/roles/viewRole.ts @@ -0,0 +1,54 @@ +import { + cleanup, + extractErrorMessage, + formatRoleView, + login, + logger, + prompt, + roleViewTable, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import type { RoleView } from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +async function viewRoleExample() { + const vault = await login() + + try { + const identifier = (await prompt('Role name or ID: ')).trim() + if (!identifier) { + logger.error('Role name or ID is required.') + process.exitCode = 1 + return + } + + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + const verbose = !asJson && isYes(await prompt('Show user/node IDs? [y/N]: ')) + + let view: RoleView + const restore = suppressLogs() + try { + view = await vault.viewRole(identifier) + } finally { + restore() + } + + if (asJson) { + logger.info(JSON.stringify(view, null, 2)) + return + } + + const table = formatRoleView(view, { verbose }) + logger.info('') + logger.info(roleViewTable(table)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(viewRoleExample) \ No newline at end of file diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index 011c1d3..3e046d5 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -189,6 +189,36 @@ export const roleEnforcementAddCommand = ( request: RoleEnforcementAddRequest ): RestCommand => createCommand(request, 'role_enforcement_add') +export type EnterpriseAllocateIdsRequest = { + number_requested: number +} + +export const enterpriseAllocateIdsCommand = ( + request: EnterpriseAllocateIdsRequest +): RestCommand => + createCommand(request, 'enterprise_allocate_ids') + +export type RoleEditRequest = { + role_id: number + node_id: number + encrypted_data: string + visible_below: boolean + new_user_inherit: boolean +} + +export const roleAddCommand = (request: RoleEditRequest): RestCommand => + createCommand(request, 'role_add') + +export const roleUpdateCommand = (request: RoleEditRequest): RestCommand => + createCommand(request, 'role_update') + +export type RoleDeleteRequest = { + role_id: number +} + +export const roleDeleteCommand = (request: RoleDeleteRequest): RestCommand => + createCommand(request, 'role_delete') + export type MoveRequest = { to_type: 'user_folder' | 'shared_folder' | 'shared_folder_folder' to_uid?: string From 2b8ef8b2c6540cfdb029cf75e461c14a52868dda Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 4 Jun 2026 23:53:35 +0530 Subject: [PATCH 02/14] Pattern validation and enterprise data improvement --- KeeperSdk/src/index.ts | 2 ++ KeeperSdk/src/roles/RoleManager.ts | 30 ++++++++++++++++++++++++++---- KeeperSdk/src/roles/listRoles.ts | 8 +++++--- KeeperSdk/src/roles/roleTypes.ts | 3 +++ KeeperSdk/src/utils/constants.ts | 5 +++++ KeeperSdk/src/utils/index.ts | 2 ++ KeeperSdk/src/utils/patterns.ts | 12 ++++++++++++ 7 files changed, 55 insertions(+), 7 deletions(-) diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 6c99d81..95c8941 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -28,6 +28,7 @@ export { ResultCodes, AuthErrorCode, SessionErrorCode, + ValidationErrorCode, RoleErrorCode, TeamErrorCode, UserErrorCode, @@ -41,6 +42,7 @@ export { EMAIL_PATTERN, EMAIL_LIST_SEPARATOR_PATTERN, isValidEmail, + resolveSearchPattern, } from './utils' export type { ILogger, Nullable, Optional, DeepPartial, Immutable } from './utils' diff --git a/KeeperSdk/src/roles/RoleManager.ts b/KeeperSdk/src/roles/RoleManager.ts index 1894360..673bb2c 100644 --- a/KeeperSdk/src/roles/RoleManager.ts +++ b/KeeperSdk/src/roles/RoleManager.ts @@ -1,5 +1,6 @@ import type { Auth } from '@keeper-security/keeperapi' import { KeeperSdkError, ResultCodes } from '../utils' +import { EnterpriseDataManager } from '../teams/enterpriseData' import { formatRolesTable, listRoles, renderRolesAsciiTable } from './listRoles' import type { FormatRolesTableOptions, @@ -44,13 +45,17 @@ export type AuthProvider = () => Auth export class RoleManager { private readonly authProvider: AuthProvider + private enterpriseData: EnterpriseDataManager | null = null constructor(authProvider: AuthProvider) { this.authProvider = authProvider } public async listRoles(options: ListRolesOptions = {}): Promise { - return listRoles(this.requireAuth(), options) + return listRoles(this.requireAuth(), { + ...options, + enterpriseData: options.enterpriseData ?? this.getEnterpriseData(), + }) } public formatRolesTable(rows: ListRoleRow[], options: FormatRolesTableOptions = {}): FormattedRolesTable { @@ -77,7 +82,9 @@ export class RoleManager { } public async addRoles(input: AddRoleInput): Promise { - return addRoles(this.requireAuth(), input) + const result = await addRoles(this.requireAuth(), input) + if (result.created > 0) this.invalidateEnterpriseData() + return result } public formatAddRoleResult(result: AddRoleResult): FormattedAddRoleTable { @@ -89,7 +96,9 @@ export class RoleManager { } public async updateRoles(input: UpdateRoleInput): Promise { - return updateRoles(this.requireAuth(), input) + const result = await updateRoles(this.requireAuth(), input) + if (result.updated > 0) this.invalidateEnterpriseData() + return result } public formatUpdateRoleResult(result: UpdateRoleResult): FormattedUpdateRoleTable { @@ -101,7 +110,9 @@ export class RoleManager { } public async deleteRoles(input: DeleteRoleInput): Promise { - return deleteRoles(this.requireAuth(), input) + const result = await deleteRoles(this.requireAuth(), input) + if (result.deleted > 0) this.invalidateEnterpriseData() + return result } public formatDeleteRoleResult(result: DeleteRoleResult): FormattedDeleteRoleTable { @@ -112,6 +123,17 @@ export class RoleManager { return renderDeleteRoleAsciiTable(table) } + private getEnterpriseData(): EnterpriseDataManager { + if (!this.enterpriseData) { + this.enterpriseData = new EnterpriseDataManager(this.requireAuth()) + } + return this.enterpriseData + } + + private invalidateEnterpriseData(): void { + this.enterpriseData?.clearCache() + } + private requireAuth(): Auth { const auth = this.authProvider() if (!auth) { diff --git a/KeeperSdk/src/roles/listRoles.ts b/KeeperSdk/src/roles/listRoles.ts index e344fff..efa4c1d 100644 --- a/KeeperSdk/src/roles/listRoles.ts +++ b/KeeperSdk/src/roles/listRoles.ts @@ -1,8 +1,9 @@ import type { Auth } from '@keeper-security/keeperapi' -import { isNumber, TOKEN_SEPARATOR_PATTERN } from '../utils' +import { isNumber, resolveSearchPattern, TOKEN_SEPARATOR_PATTERN } from '../utils' import { EnterpriseDataInclude, EnterpriseDataManager, + type EnterpriseDataManagerApi, type EnterpriseDisplayNames, type EnterpriseNode, type EnterpriseRole, @@ -55,7 +56,8 @@ export async function listRoles(auth: Auth, options: ListRolesOptions = {}): Pro columns.includes(RoleColumn.Teams) || columns.includes(RoleColumn.TeamCount) - const enterpriseData = new EnterpriseDataManager(auth) + const enterpriseData: EnterpriseDataManagerApi = + options.enterpriseData ?? new EnterpriseDataManager(auth) const emptyDisplayNames: EnterpriseDisplayNames = { nodes: new Map(), roles: new Map() } const [response, displayNames] = await Promise.all([ enterpriseData.getData(includes), @@ -80,7 +82,7 @@ export async function listRoles(auth: Auth, options: ListRolesOptions = {}): Pro teamNameById: buildTeamNameMap(response.teams || []), } - const pattern = options.pattern?.trim() || null + const pattern = resolveSearchPattern(options.pattern) const rows: ListRoleRow[] = [] for (const role of roles) { const row = decorateRow(role, columns, context) diff --git a/KeeperSdk/src/roles/roleTypes.ts b/KeeperSdk/src/roles/roleTypes.ts index a21b86f..09f95e0 100644 --- a/KeeperSdk/src/roles/roleTypes.ts +++ b/KeeperSdk/src/roles/roleTypes.ts @@ -1,3 +1,5 @@ +import type { EnterpriseDataManagerApi } from '../teams/enterpriseData' + export enum RoleColumn { VisibleBelow = 'visible_below', DefaultRole = 'default_role', @@ -25,6 +27,7 @@ export const ALL_COLUMNS_WILDCARD = '*' as const export type ListRolesOptions = { pattern?: string | null columns?: RoleColumnInput[] | typeof ALL_COLUMNS_WILDCARD | string | null + enterpriseData?: EnterpriseDataManagerApi } export type ListRoleRow = { diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index d083924..24d29f8 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -29,6 +29,10 @@ export enum SessionErrorCode { SessionTokenExpired = 'session_token_expired', } +export enum ValidationErrorCode { + InvalidPattern = 'invalid_pattern', +} + export enum RoleErrorCode { RoleRequired = 'role_required', RoleNotFound = 'role_not_found', @@ -98,6 +102,7 @@ export const ResultCodes = { NO_CLONE_CODE: SessionErrorCode.NoCloneCode, PERSISTENT_LOGIN_FAILED: SessionErrorCode.PersistentLoginFailed, SESSION_TOKEN_EXPIRED: SessionErrorCode.SessionTokenExpired, + INVALID_PATTERN: ValidationErrorCode.InvalidPattern, ROLE_REQUIRED: RoleErrorCode.RoleRequired, ROLE_NOT_FOUND: RoleErrorCode.RoleNotFound, MULTIPLE_ROLE_MATCHES: RoleErrorCode.MultipleRoleMatches, diff --git a/KeeperSdk/src/utils/index.ts b/KeeperSdk/src/utils/index.ts index 74fc4f1..a7a638f 100644 --- a/KeeperSdk/src/utils/index.ts +++ b/KeeperSdk/src/utils/index.ts @@ -4,6 +4,7 @@ export { ResultCodes, AuthErrorCode, SessionErrorCode, + ValidationErrorCode, RoleErrorCode, TeamErrorCode, UserErrorCode, @@ -21,4 +22,5 @@ export { REGEX_ESCAPE_PATTERN, isValidEmail, escapeRegExp, + resolveSearchPattern, } from './patterns' diff --git a/KeeperSdk/src/utils/patterns.ts b/KeeperSdk/src/utils/patterns.ts index bc11629..d65cc00 100644 --- a/KeeperSdk/src/utils/patterns.ts +++ b/KeeperSdk/src/utils/patterns.ts @@ -1,3 +1,6 @@ +import { KeeperSdkError } from './errors' +import { ResultCodes } from './constants' + export const EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+\.[^\s@]+$/ /** Splits user-entered email lists on whitespace, commas, and semicolons. */ @@ -18,3 +21,12 @@ export function isValidEmail(value: string): boolean { export function escapeRegExp(value: string): string { return value.replace(REGEX_ESCAPE_PATTERN, '\\$&') } + +export function resolveSearchPattern(pattern: unknown): string | null { + if (pattern == null) return null + if (typeof pattern !== 'string') { + throw new KeeperSdkError('Pattern must be a string.', ResultCodes.INVALID_PATTERN) + } + const trimmed = pattern.trim() + return trimmed.length > 0 ? trimmed : null +} From 4450c415283bae073ae2e8f7f1372a696d42728a Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Fri, 5 Jun 2026 17:16:40 +0530 Subject: [PATCH 03/14] version update and enforcement remove fix --- KeeperSdk/package-lock.json | 12 ++--- KeeperSdk/package.json | 2 +- KeeperSdk/src/roles/roleUtils.ts | 75 ++++++++++++++++++++++++++++--- KeeperSdk/src/roles/updateRole.ts | 11 +++-- keeperapi/src/commands.ts | 14 ++++++ 5 files changed, 99 insertions(+), 15 deletions(-) diff --git a/KeeperSdk/package-lock.json b/KeeperSdk/package-lock.json index 407c2de..0eed8f1 100644 --- a/KeeperSdk/package-lock.json +++ b/KeeperSdk/package-lock.json @@ -1,15 +1,15 @@ { "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@keeper-security/keeper-sdk-javascript", - "version": "1.0.0", + "version": "1.1.0", "license": "ISC", "dependencies": { - "@keeper-security/keeperapi": "17.1.3", + "@keeper-security/keeperapi": "17.2.3", "ts-node": "^10.7.0", "typescript": "^4.6.3" }, @@ -56,9 +56,9 @@ } }, "node_modules/@keeper-security/keeperapi": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.1.3.tgz", - "integrity": "sha512-xLRST98iT/aoF5gyYjMuOxXXtNQdOFWjrkfx+gxNo9SbSLFlNCeXLGsCSux23AsNXKvyAQnrmDmg04oAUFWPvA==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@keeper-security/keeperapi/-/keeperapi-17.2.3.tgz", + "integrity": "sha512-RT1ZnvfonFrPuLij8mPso/1eyTzurm2ikJxydzOA7oLQlqNRcH2FRramCan0sN85U5RNnM5QS05NVaJq4K72pg==", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", diff --git a/KeeperSdk/package.json b/KeeperSdk/package.json index f252b53..62da5b2 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -21,7 +21,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@keeper-security/keeperapi": "17.1.3", + "@keeper-security/keeperapi": "17.2.3", "ts-node": "^10.7.0", "typescript": "^4.6.3" }, diff --git a/KeeperSdk/src/roles/roleUtils.ts b/KeeperSdk/src/roles/roleUtils.ts index 4a973f7..ab8519c 100644 --- a/KeeperSdk/src/roles/roleUtils.ts +++ b/KeeperSdk/src/roles/roleUtils.ts @@ -1,6 +1,17 @@ -import { roleEnforcementAddCommand, type Auth, type KeeperResponse } from '@keeper-security/keeperapi' +import { + roleEnforcementAddCommand, + roleEnforcementRemoveCommand, + roleEnforcementUpdateCommand, + type Auth, + type KeeperResponse, +} from '@keeper-security/keeperapi' import { KeeperSdkError, ResultCodes } from '../utils' -import { EnterpriseDataManager, type EnterpriseNode, type EnterpriseRole } from '../teams/enterpriseData' +import { + EnterpriseDataManager, + type EnterpriseNode, + type EnterpriseRole, + type EnterpriseRoleEnforcementLink, +} from '../teams/enterpriseData' export const ROLE_TABLE_HEADERS = ['#', 'Status', 'Role Name', 'Role ID', 'Node ID', 'Detail'] as const export const NODE_PATH_SEPARATOR = '\\' @@ -115,12 +126,66 @@ export function assertCommandSucceeded(response: KeeperResponse, fallbackMessage } } -export async function applyRoleEnforcements(auth: Auth, roleId: number, pairs: EnforcementPair[]): Promise { +const ENFORCEMENT_FALSE = new Set(['false', 'f', '0', 'off', 'no', 'n']) +const ENFORCEMENT_TRUE = new Set(['true', 't', '1', 'on', 'yes', 'y']) + +function normalizeEnforcementKey(key: string): string { + return key.trim().toLowerCase().replace(/-/g, '_') +} + +function roleHasEnforcement( + roleId: number, + enforcement: string, + links: readonly EnterpriseRoleEnforcementLink[] +): boolean { + return links.some( + (link) => + link.role_id === roleId && normalizeEnforcementKey(link.enforcement_type) === enforcement + ) +} + +export async function applyRoleEnforcements( + auth: Auth, + roleId: number, + pairs: EnforcementPair[], + existingLinks: readonly EnterpriseRoleEnforcementLink[] = [] +): Promise { for (const { key, value } of pairs) { + const enforcement = normalizeEnforcementKey(key) + const lower = value.trim().toLowerCase() + + if (ENFORCEMENT_FALSE.has(lower)) { + if (!roleHasEnforcement(roleId, enforcement, existingLinks)) continue + const response = await auth.executeRestCommand( + roleEnforcementRemoveCommand({ role_id: roleId, enforcement }) + ) + assertCommandSucceeded( + response, + `role_enforcement_remove failed: ${enforcement}`, + ResultCodes.ROLE_ENFORCEMENT_FAILED + ) + continue + } + + if (ENFORCEMENT_TRUE.has(lower)) { + const exists = roleHasEnforcement(roleId, enforcement, existingLinks) + const response = await auth.executeRestCommand( + exists + ? roleEnforcementUpdateCommand({ role_id: roleId, enforcement }) + : roleEnforcementAddCommand({ role_id: roleId, enforcement }) + ) + assertCommandSucceeded( + response, + `role_enforcement_${exists ? 'update' : 'add'} failed: ${enforcement}`, + ResultCodes.ROLE_ENFORCEMENT_FAILED + ) + continue + } + const response = await auth.executeRestCommand( - roleEnforcementAddCommand({ role_id: roleId, enforcement: key, value }) + roleEnforcementAddCommand({ role_id: roleId, enforcement, value }) ) - assertCommandSucceeded(response, `role_enforcement_add failed: ${key}`, ResultCodes.ROLE_ENFORCEMENT_FAILED) + assertCommandSucceeded(response, `role_enforcement_add failed: ${enforcement}`, ResultCodes.ROLE_ENFORCEMENT_FAILED) } } diff --git a/KeeperSdk/src/roles/updateRole.ts b/KeeperSdk/src/roles/updateRole.ts index a0bee3f..d0fc567 100644 --- a/KeeperSdk/src/roles/updateRole.ts +++ b/KeeperSdk/src/roles/updateRole.ts @@ -26,7 +26,10 @@ import { type RoleToggleInput, } from './roleUtils' -const UPDATE_ROLE_INCLUDES: EnterpriseDataInclude[] = [EnterpriseDataInclude.Nodes, EnterpriseDataInclude.Roles] +const UPDATE_ROLE_BASE_INCLUDES: EnterpriseDataInclude[] = [ + EnterpriseDataInclude.Nodes, + EnterpriseDataInclude.Roles, +] export enum UpdateRoleStatus { Updated = 'updated', @@ -80,10 +83,12 @@ export async function updateRoles(auth: Auth, input: UpdateRoleInput): Promise 0) includes.push(EnterpriseDataInclude.RoleEnforcements) const enterpriseData = new EnterpriseDataManager(auth) const [response, displayNames] = await Promise.all([ - enterpriseData.getData(UPDATE_ROLE_INCLUDES), + enterpriseData.getData(includes), enterpriseData.getDisplayNames(), ]) @@ -118,7 +123,7 @@ export async function updateRoles(auth: Auth, input: UpdateRoleInput): Promise => createCommand(request, 'role_enforcement_add') +export const roleEnforcementUpdateCommand = ( + request: RoleEnforcementAddRequest +): RestCommand => createCommand(request, 'role_enforcement_update') + +export type RoleEnforcementRemoveRequest = { + role_id: number + enforcement: string +} + +export const roleEnforcementRemoveCommand = ( + request: RoleEnforcementRemoveRequest +): RestCommand => + createCommand(request, 'role_enforcement_remove') + export type EnterpriseAllocateIdsRequest = { number_requested: number } From bf89eeac7af64fb83cf482223c719a1c70b0bbf2 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Fri, 5 Jun 2026 21:32:50 +0530 Subject: [PATCH 04/14] Prettier format change --- keeperapi/src/commands.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index 8d9c042..4b9bff2 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -200,8 +200,7 @@ export type RoleEnforcementRemoveRequest = { export const roleEnforcementRemoveCommand = ( request: RoleEnforcementRemoveRequest -): RestCommand => - createCommand(request, 'role_enforcement_remove') +): RestCommand => createCommand(request, 'role_enforcement_remove') export type EnterpriseAllocateIdsRequest = { number_requested: number From 45152ae607bf8f959da11ba99fecd19eb32ba10a Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Fri, 5 Jun 2026 21:55:51 +0530 Subject: [PATCH 05/14] Refactor roles module exports --- KeeperSdk/src/index.ts | 65 +++++++++++--------------- KeeperSdk/src/roles/index.ts | 74 ++++++++++++++++++++++++++++++ KeeperSdk/src/roles/viewRole.ts | 31 +++++++------ KeeperSdk/src/vault/KeeperVault.ts | 18 +++++--- 4 files changed, 128 insertions(+), 60 deletions(-) create mode 100644 KeeperSdk/src/roles/index.ts diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 95c8941..5e93ce1 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -223,22 +223,38 @@ export type { NodePathOptions, } from './teams/enterpriseData' -export { listRoles, formatRolesTable, renderRolesAsciiTable } from './roles/listRoles' export { + listRoles, + formatRolesTable, + renderRolesAsciiTable, RoleColumn, SUPPORTED_ROLE_COLUMNS, DEFAULT_ROLE_COLUMNS, -} from './roles/roleTypes' + ALL_COLUMNS_WILDCARD, + viewRole, + formatRoleView, + roleViewTable, + RoleManager, + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + AddRoleStatus, + AddRoleSkipReason, + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + UpdateRoleStatus, + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + DeleteRoleStatus, +} from './roles' export type { ListRolesOptions, ListRoleRow, RoleColumnInput, FormattedRolesTable, FormatRolesTableOptions, -} from './roles/roleTypes' - -export { viewRole, formatRoleView, roleViewTable } from './roles/viewRole' -export type { RoleView, RoleTeamInfo, RoleUserInfo, @@ -248,53 +264,24 @@ export type { FormattedRoleViewTable, FormattedManagedNodePrivilegeTable, RoleViewTableRow, -} from './roles/viewRole' - -export { RoleManager } from './roles/RoleManager' - -export { - addRoles, - formatAddRoleResult, - renderAddRoleAsciiTable, - AddRoleStatus, - AddRoleSkipReason, -} from './roles/addRole' -export type { AddRoleInput, AddRoleResult, AddRoleItemResult, AddRoleConfirm, AddRoleConflictPrompt, FormattedAddRoleTable, -} from './roles/addRole' - -export { - updateRoles, - formatUpdateRoleResult, - renderUpdateRoleAsciiTable, - UpdateRoleStatus, -} from './roles/updateRole' -export type { UpdateRoleInput, UpdateRoleResult, UpdateRoleItemResult, FormattedUpdateRoleTable, -} from './roles/updateRole' - -export { - deleteRoles, - formatDeleteRoleResult, - renderDeleteRoleAsciiTable, - DeleteRoleStatus, -} from './roles/deleteRole' -export type { DeleteRoleInput, DeleteRoleResult, DeleteRoleItemResult, FormattedDeleteRoleTable, -} from './roles/deleteRole' - -export type { RoleToggleInput, RoleToggle, EnforcementPair } from './roles/roleUtils' + RoleToggleInput, + RoleToggle, + EnforcementPair, +} from './roles' export { viewTeam, formatTeamView, teamViewTable } from './teams/viewTeam' export type { diff --git a/KeeperSdk/src/roles/index.ts b/KeeperSdk/src/roles/index.ts new file mode 100644 index 0000000..b0e3b99 --- /dev/null +++ b/KeeperSdk/src/roles/index.ts @@ -0,0 +1,74 @@ +export { listRoles, formatRolesTable, renderRolesAsciiTable } from './listRoles' + +export { + RoleColumn, + SUPPORTED_ROLE_COLUMNS, + DEFAULT_ROLE_COLUMNS, + ALL_COLUMNS_WILDCARD, +} from './roleTypes' +export type { + ListRolesOptions, + ListRoleRow, + RoleColumnInput, + FormattedRolesTable, + FormatRolesTableOptions, +} from './roleTypes' + +export { viewRole, formatRoleView, roleViewTable } from './viewRole' +export type { + RoleView, + RoleTeamInfo, + RoleUserInfo, + RoleManagedNodeInfo, + RoleEnforcementInfo, + FormatRoleViewOptions, + FormattedRoleViewTable, + FormattedManagedNodePrivilegeTable, + RoleViewTableRow, +} from './viewRole' + +export { RoleManager } from './RoleManager' + +export { + addRoles, + formatAddRoleResult, + renderAddRoleAsciiTable, + AddRoleStatus, + AddRoleSkipReason, +} from './addRole' +export type { + AddRoleInput, + AddRoleResult, + AddRoleItemResult, + AddRoleConfirm, + AddRoleConflictPrompt, + FormattedAddRoleTable, +} from './addRole' + +export { + updateRoles, + formatUpdateRoleResult, + renderUpdateRoleAsciiTable, + UpdateRoleStatus, +} from './updateRole' +export type { + UpdateRoleInput, + UpdateRoleResult, + UpdateRoleItemResult, + FormattedUpdateRoleTable, +} from './updateRole' + +export { + deleteRoles, + formatDeleteRoleResult, + renderDeleteRoleAsciiTable, + DeleteRoleStatus, +} from './deleteRole' +export type { + DeleteRoleInput, + DeleteRoleResult, + DeleteRoleItemResult, + FormattedDeleteRoleTable, +} from './deleteRole' + +export type { RoleToggleInput, RoleToggle, EnforcementPair } from './roleUtils' diff --git a/KeeperSdk/src/roles/viewRole.ts b/KeeperSdk/src/roles/viewRole.ts index b90e4a4..e0aa336 100644 --- a/KeeperSdk/src/roles/viewRole.ts +++ b/KeeperSdk/src/roles/viewRole.ts @@ -366,18 +366,17 @@ function renderMainSection(table: FormattedRoleViewTable): string { ? expandedRows.reduce((max, row) => Math.max(max, ...row.idLines.map((l) => l.length)), 0) : 0 - const padRight = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length)) - const padLeft = (text: string, width: number) => ' '.repeat(Math.max(0, width - text.length)) + text - + const columnWidths = hasIdColumn ? [labelWidth, valueWidth, idWidth] : [labelWidth, valueWidth] const lines: string[] = [] for (const row of expandedRows) { const lineCount = Math.max(row.valueLines.length, hasIdColumn ? row.idLines.length : 0, 1) for (let li = 0; li < lineCount; li++) { - const labelCell = padLeft(li === 0 ? row.label : '', labelWidth) - const valueCell = padRight(row.valueLines[li] ?? '', valueWidth) - const cells: string[] = [labelCell, valueCell] - if (hasIdColumn) cells.push(padRight(row.idLines[li] ?? '', idWidth)) - lines.push(cells.join(' ').trimEnd()) + const cells = [ + li === 0 ? row.label : '', + row.valueLines[li] ?? '', + ...(hasIdColumn ? [row.idLines[li] ?? ''] : []), + ] + lines.push(formatAsciiRow(cells, columnWidths)) } } return lines.join('\n') @@ -389,14 +388,13 @@ function renderEnforcementsSection(rows: RoleEnforcementInfo[]): string { const nameWidth = Math.max('Name'.length, ...rows.map((r) => r.name.length)) const valWidth = Math.max('Value'.length, ...rows.map((r) => r.value.length)) + const columnWidths = [nameWidth, valWidth] - const pad = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length)) - const rule = `${'-'.repeat(nameWidth)} ${'-'.repeat(valWidth)}` const lines: string[] = [ title, - `${pad('Name', nameWidth)} ${'Value'}`, - rule, - ...rows.map((r) => `${pad(r.name, nameWidth)} ${r.value}`), + formatAsciiRow(['Name', 'Value'], columnWidths), + formatAsciiRow(['-'.repeat(nameWidth), '-'.repeat(valWidth)], columnWidths), + ...rows.map((r) => formatAsciiRow([r.name, r.value], columnWidths)), ] return lines.join('\n') } @@ -414,8 +412,7 @@ function renderManagedNodesSection(table: FormattedManagedNodePrivilegeTable): s } colWidths[0] = Math.max(colWidths[0], 'Cascade Node Permissions'.length) - const pad = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length)) - const formatRow = (cells: string[]) => cells.map((c, i) => pad(c, colWidths[i])).join(' ').trimEnd() + const formatRow = (cells: string[]) => formatAsciiRow(cells, colWidths) const rule = formatRow(colWidths.map((w) => '-'.repeat(w))) const lines: string[] = [ @@ -444,4 +441,8 @@ function asIdLines(id: number | number[] | undefined): string[] { if (id == null) return [] if (Array.isArray(id)) return id.length > 0 ? id.map(String) : [''] return [String(id)] +} + +function formatAsciiRow(cells: string[], widths: number[]): string { + return cells.map((cell, index) => cell + ' '.repeat(Math.max(0, widths[index] - cell.length))).join(' ') } \ No newline at end of file diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 0c9cd11..52c3055 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -62,12 +62,18 @@ import type { TeamView } from '../teams/viewTeam' import type { AddTeamInput, AddTeamResult } from '../teams/addTeam' import type { UpdateTeamInput, UpdateTeamResult } from '../teams/updateTeam' import type { DeleteTeamInput, DeleteTeamResult } from '../teams/deleteTeam' -import { RoleManager } from '../roles/RoleManager' -import type { ListRoleRow, ListRolesOptions } from '../roles/roleTypes' -import type { RoleView } from '../roles/viewRole' -import type { AddRoleInput, AddRoleResult } from '../roles/addRole' -import type { UpdateRoleInput, UpdateRoleResult } from '../roles/updateRole' -import type { DeleteRoleInput, DeleteRoleResult } from '../roles/deleteRole' +import { + RoleManager, + type AddRoleInput, + type AddRoleResult, + type DeleteRoleInput, + type DeleteRoleResult, + type ListRoleRow, + type ListRolesOptions, + type RoleView, + type UpdateRoleInput, + type UpdateRoleResult, +} from '../roles' import { UserManager } from '../users/UserManager' import type { ListUserRow, From ce34df7a9fb33f6ed4d50a4341560b6bca71ba46 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Mon, 8 Jun 2026 22:22:46 +0530 Subject: [PATCH 06/14] Add role enforcement and CRUD commands to keeperapi (#165) --- KeeperSdk/package.json | 2 +- keeperapi/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/KeeperSdk/package.json b/KeeperSdk/package.json index 62da5b2..7a1fe81 100644 --- a/KeeperSdk/package.json +++ b/KeeperSdk/package.json @@ -21,7 +21,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@keeper-security/keeperapi": "17.2.3", + "@keeper-security/keeperapi": "17.2.6", "ts-node": "^10.7.0", "typescript": "^4.6.3" }, diff --git a/keeperapi/package.json b/keeperapi/package.json index caac0a3..2004f4c 100644 --- a/keeperapi/package.json +++ b/keeperapi/package.json @@ -1,7 +1,7 @@ { "name": "@keeper-security/keeperapi", "description": "Keeper API Javascript SDK", - "version": "17.2.3", + "version": "17.2.6", "browser": "dist/index.es.js", "main": "dist/index.cjs.js", "types": "dist/node/index.d.ts", From 3ea7ff9b69ea51d5fcda7b73fe698dfcfa8bc512 Mon Sep 17 00:00:00 2001 From: Hoseong Lee <154545063+hleekeeper@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:45:48 -0500 Subject: [PATCH 07/14] BE-6873 Support Keeper Drive for BE's v1 (#154) * BE-6955 Added new REST API callers keeperDriveRecordsAdd and keeperDriveRecordsUpdate (#123) * Updated the protobuf generation commands * Updated protobuf files * Added keeperDriveRecordsAdd and keeperDriveRecordsUpdate * Updated the protobufs * Ran format:check * fixed the wrong namespace used for the keeperDriveRecordsAdd caller * BE-6875 Support ingesting Keeper Drive records (#126) * Added isNil and toOptional * Added syncDown module * Support ingesting Keeper Drive records * fixed the broken unit tests * added a safety check * update * BE-6875 Support ingesting Keeper Drive folders (#128) * Added types DKdFolder, DKdFolderAccess, and DKdFolderSharingState * Added a method mapTeamKeyType * Ingest the Keeper Drive folder data * addressed a comment * Ingest keeper drive record links data (#129) * BE-6875 Added another optional param to the storage.delete function (#130) * Added another optional param to the storage.delete function * updated the code format * Handle removing the kd record links (#133) * Added the accessUid to the DKdFolderAccess type (#134) * BE-6875 Additional changes to ingest KD folder data (#135) * minor change uid -> folderUid in the DKdFolderAccesst type * A removed dependency can now explicitly specify parentKind and childKind * ingest non-shared-data for keeper drive items (#147) * Ingest the securityScoreData, securitData, and breachWatchRecords from the keeperDriveData (#150) Updated the getCounts to count the data in the keeperDriveData * Re-ran the protobug generation command --- keeperapi/package.json | 4 +- .../src/__tests__/SyncDownResponseBuilder.ts | 1 + .../src/__tests__/syncDown/utils.test.ts | 13 + keeperapi/src/browser/index.ts | 1 + keeperapi/src/node/index.ts | 1 + keeperapi/src/proto.d.ts | 756 +++++ keeperapi/src/proto.js | 2431 +++++++++++++++++ keeperapi/src/restMessages.ts | 11 + keeperapi/src/syncDown/index.ts | 2 + keeperapi/src/syncDown/types.ts | 80 + keeperapi/src/syncDown/utils.ts | 30 + keeperapi/src/utils.ts | 8 + keeperapi/src/vault.ts | 543 +++- 13 files changed, 3836 insertions(+), 45 deletions(-) create mode 100644 keeperapi/src/__tests__/syncDown/utils.test.ts create mode 100644 keeperapi/src/syncDown/index.ts create mode 100644 keeperapi/src/syncDown/types.ts create mode 100644 keeperapi/src/syncDown/utils.ts diff --git a/keeperapi/package.json b/keeperapi/package.json index 2004f4c..fe14723 100644 --- a/keeperapi/package.json +++ b/keeperapi/package.json @@ -10,8 +10,8 @@ "scripts": { "start": "rollup -cw", "build": "node ./scripts/cleanDistFolder.js && rollup -c && cp src/proto.d.ts dist", - "update-proto:es6": "pbjs -t static-module --force-number -w es6 -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto && pbts -o src/proto.d.ts src/proto.js", - "update-proto:cjs": "pbjs -t json-module -w commonjs -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/router.proto && pbjs -t static-module --force-number -w commonjs ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto | pbts -o src/proto.d.ts -", + "update-proto:es6": "pbjs -t static-module --force-number -w es6 -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto ../../keeperapp-protobuf/record_endpoints.proto && pbts -o src/proto.d.ts src/proto.js", + "update-proto:cjs": "pbjs -t json-module -w commonjs -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto ../../keeperapp-protobuf/record_endpoints.proto && pbjs -t static-module -w commonjs ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto ../../keeperapp-protobuf/record_endpoints.proto | pbts -o src/proto.d.ts -", "format": "prettier --write .", "format:check": "prettier --check .", "test": "jest", diff --git a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts index aee3cbb..aa40cfb 100644 --- a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts +++ b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts @@ -78,6 +78,7 @@ export class SyncDownResponseBuilder { removedSharedFolders: [], removedUsers: [], recordRotations: [], + keeperDriveData: {}, } } diff --git a/keeperapi/src/__tests__/syncDown/utils.test.ts b/keeperapi/src/__tests__/syncDown/utils.test.ts new file mode 100644 index 0000000..5529606 --- /dev/null +++ b/keeperapi/src/__tests__/syncDown/utils.test.ts @@ -0,0 +1,13 @@ +import { createKdRecordAccessCompositeKey, createKdFolderAccessCompositeKey } from '../../syncDown' + +describe('createKdRecordAccessCompositeKey', () => { + it('produces actorUid:recordUid format', () => { + expect(createKdRecordAccessCompositeKey('actor1', 'record1')).toBe('actor1:record1') + }) +}) + +describe('createKdFolderAccessCompositeKey', () => { + it('produces folderUid:actorUid format', () => { + expect(createKdFolderAccessCompositeKey('actor1', 'folder1')).toBe('folder1:actor1') + }) +}) diff --git a/keeperapi/src/browser/index.ts b/keeperapi/src/browser/index.ts index c3ac417..4daff36 100644 --- a/keeperapi/src/browser/index.ts +++ b/keeperapi/src/browser/index.ts @@ -14,6 +14,7 @@ export * from '../proto' export * from '../cryptoWorker' export * from '../qrc' export * from '../pam' +export * from '../syncDown' import { connectPlatform } from '../platform' import { browserPlatform } from './platform' diff --git a/keeperapi/src/node/index.ts b/keeperapi/src/node/index.ts index 81935c7..b6f90fe 100644 --- a/keeperapi/src/node/index.ts +++ b/keeperapi/src/node/index.ts @@ -17,6 +17,7 @@ export * from '../proto' export * from '../cryptoWorker' export * from '../qrc' export * from '../pam' +export * from '../syncDown' import { connectPlatform } from '../platform' import { nodePlatform } from './platform' diff --git a/keeperapi/src/proto.d.ts b/keeperapi/src/proto.d.ts index cc0347e..3d4ea78 100644 --- a/keeperapi/src/proto.d.ts +++ b/keeperapi/src/proto.d.ts @@ -1711,6 +1711,139 @@ export namespace Authentication { public static getTypeUrl(typeUrlPrefix?: string): string; } + /** Properties of a KeysInfo. */ + interface IKeysInfo { + + /** KeysInfo encryptionParams */ + encryptionParams?: (Uint8Array|null); + + /** KeysInfo encryptedDataKey */ + encryptedDataKey?: (Uint8Array|null); + + /** KeysInfo dataKeyBackupDate */ + dataKeyBackupDate?: (number|null); + + /** KeysInfo userAuthUid */ + userAuthUid?: (Uint8Array|null); + + /** KeysInfo encryptedPrivateKey */ + encryptedPrivateKey?: (Uint8Array|null); + + /** KeysInfo encryptedEccPrivateKey */ + encryptedEccPrivateKey?: (Uint8Array|null); + + /** KeysInfo eccPublicKey */ + eccPublicKey?: (Uint8Array|null); + } + + /** Represents a KeysInfo. */ + class KeysInfo implements IKeysInfo { + + /** + * Constructs a new KeysInfo. + * @param [properties] Properties to set + */ + constructor(properties?: Authentication.IKeysInfo); + + /** KeysInfo encryptionParams. */ + public encryptionParams: Uint8Array; + + /** KeysInfo encryptedDataKey. */ + public encryptedDataKey: Uint8Array; + + /** KeysInfo dataKeyBackupDate. */ + public dataKeyBackupDate: number; + + /** KeysInfo userAuthUid. */ + public userAuthUid: Uint8Array; + + /** KeysInfo encryptedPrivateKey. */ + public encryptedPrivateKey: Uint8Array; + + /** KeysInfo encryptedEccPrivateKey. */ + public encryptedEccPrivateKey: Uint8Array; + + /** KeysInfo eccPublicKey. */ + public eccPublicKey: Uint8Array; + + /** + * Creates a new KeysInfo instance using the specified properties. + * @param [properties] Properties to set + * @returns KeysInfo instance + */ + public static create(properties?: Authentication.IKeysInfo): Authentication.KeysInfo; + + /** + * Encodes the specified KeysInfo message. Does not implicitly {@link Authentication.KeysInfo.verify|verify} messages. + * @param message KeysInfo message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: Authentication.IKeysInfo, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified KeysInfo message, length delimited. Does not implicitly {@link Authentication.KeysInfo.verify|verify} messages. + * @param message KeysInfo message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: Authentication.IKeysInfo, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a KeysInfo message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns KeysInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): Authentication.KeysInfo; + + /** + * Decodes a KeysInfo message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns KeysInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): Authentication.KeysInfo; + + /** + * Verifies a KeysInfo message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a KeysInfo message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns KeysInfo + */ + public static fromObject(object: { [k: string]: any }): Authentication.KeysInfo; + + /** + * Creates a plain object from a KeysInfo message. Also converts values to other types if specified. + * @param message KeysInfo + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: Authentication.KeysInfo, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this KeysInfo to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for KeysInfo + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + /** Properties of a LoginResponse. */ interface ILoginResponse { @@ -1761,6 +1894,12 @@ export namespace Authentication { /** LoginResponse sessionTokenTypeModifier */ sessionTokenTypeModifier?: (string|null); + + /** LoginResponse keysInfo */ + keysInfo?: (Authentication.IKeysInfo|null); + + /** LoginResponse clientKey */ + clientKey?: (Uint8Array|null); } /** Represents a LoginResponse. */ @@ -1820,6 +1959,12 @@ export namespace Authentication { /** LoginResponse sessionTokenTypeModifier. */ public sessionTokenTypeModifier: string; + /** LoginResponse keysInfo. */ + public keysInfo?: (Authentication.IKeysInfo|null); + + /** LoginResponse clientKey. */ + public clientKey: Uint8Array; + /** * Creates a new LoginResponse instance using the specified properties. * @param [properties] Properties to set @@ -86910,6 +87055,284 @@ export namespace record { public static getTypeUrl(typeUrlPrefix?: string): string; } } + + /** Properties of a RecordsAddRequest. */ + interface IRecordsAddRequest { + + /** RecordsAddRequest records */ + records?: (record.v3.IRecordAdd[]|null); + + /** RecordsAddRequest clientTime */ + clientTime?: (number|null); + + /** RecordsAddRequest securityDataKeyType */ + securityDataKeyType?: (Records.RecordKeyType|null); + } + + /** Represents a RecordsAddRequest. */ + class RecordsAddRequest implements IRecordsAddRequest { + + /** + * Constructs a new RecordsAddRequest. + * @param [properties] Properties to set + */ + constructor(properties?: record.v3.IRecordsAddRequest); + + /** RecordsAddRequest records. */ + public records: record.v3.IRecordAdd[]; + + /** RecordsAddRequest clientTime. */ + public clientTime: number; + + /** RecordsAddRequest securityDataKeyType. */ + public securityDataKeyType: Records.RecordKeyType; + + /** + * Creates a new RecordsAddRequest instance using the specified properties. + * @param [properties] Properties to set + * @returns RecordsAddRequest instance + */ + public static create(properties?: record.v3.IRecordsAddRequest): record.v3.RecordsAddRequest; + + /** + * Encodes the specified RecordsAddRequest message. Does not implicitly {@link record.v3.RecordsAddRequest.verify|verify} messages. + * @param message RecordsAddRequest message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: record.v3.IRecordsAddRequest, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified RecordsAddRequest message, length delimited. Does not implicitly {@link record.v3.RecordsAddRequest.verify|verify} messages. + * @param message RecordsAddRequest message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: record.v3.IRecordsAddRequest, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a RecordsAddRequest message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns RecordsAddRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): record.v3.RecordsAddRequest; + + /** + * Decodes a RecordsAddRequest message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns RecordsAddRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): record.v3.RecordsAddRequest; + + /** + * Verifies a RecordsAddRequest message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a RecordsAddRequest message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns RecordsAddRequest + */ + public static fromObject(object: { [k: string]: any }): record.v3.RecordsAddRequest; + + /** + * Creates a plain object from a RecordsAddRequest message. Also converts values to other types if specified. + * @param message RecordsAddRequest + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: record.v3.RecordsAddRequest, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this RecordsAddRequest to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for RecordsAddRequest + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + /** Properties of a RecordAdd. */ + interface IRecordAdd { + + /** RecordAdd recordUid */ + recordUid?: (Uint8Array|null); + + /** RecordAdd recordKey */ + recordKey?: (Uint8Array|null); + + /** RecordAdd recordKeyType */ + recordKeyType?: (Folder.EncryptedKeyType|null); + + /** + * Record creates in root folder is encrypted by user key. + * Record creates in non-root folder is encrypted by folder key. + */ + recordKeyEncryptedBy?: (Folder.FolderKeyEncryptionType|null); + + /** RecordAdd clientModifiedTime */ + clientModifiedTime?: (number|null); + + /** RecordAdd data */ + data?: (Uint8Array|null); + + /** RecordAdd nonSharedData */ + nonSharedData?: (Uint8Array|null); + + /** RecordAdd folderUid */ + folderUid?: (Uint8Array|null); + + /** RecordAdd recordLinks */ + recordLinks?: (Records.IRecordLink[]|null); + + /** RecordAdd audit */ + audit?: (Records.IRecordAudit|null); + + /** RecordAdd securityData */ + securityData?: (Records.ISecurityData|null); + + /** RecordAdd securityScoreData */ + securityScoreData?: (Records.ISecurityScoreData|null); + } + + /** Represents a RecordAdd. */ + class RecordAdd implements IRecordAdd { + + /** + * Constructs a new RecordAdd. + * @param [properties] Properties to set + */ + constructor(properties?: record.v3.IRecordAdd); + + /** RecordAdd recordUid. */ + public recordUid: Uint8Array; + + /** RecordAdd recordKey. */ + public recordKey: Uint8Array; + + /** RecordAdd recordKeyType. */ + public recordKeyType: Folder.EncryptedKeyType; + + /** + * Record creates in root folder is encrypted by user key. + * Record creates in non-root folder is encrypted by folder key. + */ + public recordKeyEncryptedBy: Folder.FolderKeyEncryptionType; + + /** RecordAdd clientModifiedTime. */ + public clientModifiedTime: number; + + /** RecordAdd data. */ + public data: Uint8Array; + + /** RecordAdd nonSharedData. */ + public nonSharedData: Uint8Array; + + /** RecordAdd folderUid. */ + public folderUid: Uint8Array; + + /** RecordAdd recordLinks. */ + public recordLinks: Records.IRecordLink[]; + + /** RecordAdd audit. */ + public audit?: (Records.IRecordAudit|null); + + /** RecordAdd securityData. */ + public securityData?: (Records.ISecurityData|null); + + /** RecordAdd securityScoreData. */ + public securityScoreData?: (Records.ISecurityScoreData|null); + + /** + * Creates a new RecordAdd instance using the specified properties. + * @param [properties] Properties to set + * @returns RecordAdd instance + */ + public static create(properties?: record.v3.IRecordAdd): record.v3.RecordAdd; + + /** + * Encodes the specified RecordAdd message. Does not implicitly {@link record.v3.RecordAdd.verify|verify} messages. + * @param message RecordAdd message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: record.v3.IRecordAdd, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified RecordAdd message, length delimited. Does not implicitly {@link record.v3.RecordAdd.verify|verify} messages. + * @param message RecordAdd message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: record.v3.IRecordAdd, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a RecordAdd message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns RecordAdd + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): record.v3.RecordAdd; + + /** + * Decodes a RecordAdd message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns RecordAdd + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): record.v3.RecordAdd; + + /** + * Verifies a RecordAdd message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a RecordAdd message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns RecordAdd + */ + public static fromObject(object: { [k: string]: any }): record.v3.RecordAdd; + + /** + * Creates a plain object from a RecordAdd message. Also converts values to other types if specified. + * @param message RecordAdd + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: record.v3.RecordAdd, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this RecordAdd to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for RecordAdd + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } } } @@ -102970,6 +103393,12 @@ export namespace Router { /** RouterRecordRotationRequest saasConfiguration */ saasConfiguration?: (Uint8Array|null); + + /** RouterRecordRotationRequest updateServices */ + updateServices?: (boolean|null); + + /** RouterRecordRotationRequest serviceResources */ + serviceResources?: (PAM.IUidList|null); } /** Represents a RouterRecordRotationRequest. */ @@ -103017,6 +103446,12 @@ export namespace Router { /** RouterRecordRotationRequest saasConfiguration. */ public saasConfiguration?: (Uint8Array|null); + /** RouterRecordRotationRequest updateServices. */ + public updateServices?: (boolean|null); + + /** RouterRecordRotationRequest serviceResources. */ + public serviceResources?: (PAM.IUidList|null); + /** * Creates a new RouterRecordRotationRequest instance using the specified properties. * @param [properties] Properties to set @@ -105589,6 +106024,12 @@ export namespace Router { /** PAMNetworkSettings allowedSettings */ allowedSettings?: (Uint8Array|null); + + /** PAMNetworkSettings idpConfigUid */ + idpConfigUid?: (Uint8Array|null); + + /** PAMNetworkSettings adminUid */ + adminUid?: (Uint8Array|null); } /** Represents a PAMNetworkSettings. */ @@ -105603,6 +106044,12 @@ export namespace Router { /** PAMNetworkSettings allowedSettings. */ public allowedSettings: Uint8Array; + /** PAMNetworkSettings idpConfigUid. */ + public idpConfigUid?: (Uint8Array|null); + + /** PAMNetworkSettings adminUid. */ + public adminUid?: (Uint8Array|null); + /** * Creates a new PAMNetworkSettings instance using the specified properties. * @param [properties] Properties to set @@ -109977,6 +110424,9 @@ export namespace PAM { /** PAMResourceConfig keeperAiSettings */ keeperAiSettings?: (Uint8Array|null); + + /** PAMResourceConfig updateServices */ + updateServices?: (boolean|null); } /** Represents a PAMResourceConfig. */ @@ -110015,6 +110465,9 @@ export namespace PAM { /** PAMResourceConfig keeperAiSettings. */ public keeperAiSettings?: (Uint8Array|null); + /** PAMResourceConfig updateServices. */ + public updateServices?: (boolean|null); + /** * Creates a new PAMResourceConfig instance using the specified properties. * @param [properties] Properties to set @@ -111245,4 +111698,307 @@ export namespace PAM { */ public static getTypeUrl(typeUrlPrefix?: string): string; } + + /** Properties of a PAMUniversalSyncPreCheckRequest. */ + interface IPAMUniversalSyncPreCheckRequest { + + /** PAMUniversalSyncPreCheckRequest networkUid */ + networkUid?: (Uint8Array|null); + + /** PAMUniversalSyncPreCheckRequest folderUids */ + folderUids?: (Uint8Array[]|null); + } + + /** Represents a PAMUniversalSyncPreCheckRequest. */ + class PAMUniversalSyncPreCheckRequest implements IPAMUniversalSyncPreCheckRequest { + + /** + * Constructs a new PAMUniversalSyncPreCheckRequest. + * @param [properties] Properties to set + */ + constructor(properties?: PAM.IPAMUniversalSyncPreCheckRequest); + + /** PAMUniversalSyncPreCheckRequest networkUid. */ + public networkUid: Uint8Array; + + /** PAMUniversalSyncPreCheckRequest folderUids. */ + public folderUids: Uint8Array[]; + + /** + * Creates a new PAMUniversalSyncPreCheckRequest instance using the specified properties. + * @param [properties] Properties to set + * @returns PAMUniversalSyncPreCheckRequest instance + */ + public static create(properties?: PAM.IPAMUniversalSyncPreCheckRequest): PAM.PAMUniversalSyncPreCheckRequest; + + /** + * Encodes the specified PAMUniversalSyncPreCheckRequest message. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckRequest.verify|verify} messages. + * @param message PAMUniversalSyncPreCheckRequest message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: PAM.IPAMUniversalSyncPreCheckRequest, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified PAMUniversalSyncPreCheckRequest message, length delimited. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckRequest.verify|verify} messages. + * @param message PAMUniversalSyncPreCheckRequest message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: PAM.IPAMUniversalSyncPreCheckRequest, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PAMUniversalSyncPreCheckRequest message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns PAMUniversalSyncPreCheckRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): PAM.PAMUniversalSyncPreCheckRequest; + + /** + * Decodes a PAMUniversalSyncPreCheckRequest message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns PAMUniversalSyncPreCheckRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): PAM.PAMUniversalSyncPreCheckRequest; + + /** + * Verifies a PAMUniversalSyncPreCheckRequest message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a PAMUniversalSyncPreCheckRequest message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns PAMUniversalSyncPreCheckRequest + */ + public static fromObject(object: { [k: string]: any }): PAM.PAMUniversalSyncPreCheckRequest; + + /** + * Creates a plain object from a PAMUniversalSyncPreCheckRequest message. Also converts values to other types if specified. + * @param message PAMUniversalSyncPreCheckRequest + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: PAM.PAMUniversalSyncPreCheckRequest, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PAMUniversalSyncPreCheckRequest to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for PAMUniversalSyncPreCheckRequest + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + /** Properties of a PAMUniversalSyncPreCheckResult. */ + interface IPAMUniversalSyncPreCheckResult { + + /** PAMUniversalSyncPreCheckResult folderUid */ + folderUid?: (Uint8Array|null); + + /** PAMUniversalSyncPreCheckResult isUsed */ + isUsed?: (boolean|null); + } + + /** Represents a PAMUniversalSyncPreCheckResult. */ + class PAMUniversalSyncPreCheckResult implements IPAMUniversalSyncPreCheckResult { + + /** + * Constructs a new PAMUniversalSyncPreCheckResult. + * @param [properties] Properties to set + */ + constructor(properties?: PAM.IPAMUniversalSyncPreCheckResult); + + /** PAMUniversalSyncPreCheckResult folderUid. */ + public folderUid: Uint8Array; + + /** PAMUniversalSyncPreCheckResult isUsed. */ + public isUsed: boolean; + + /** + * Creates a new PAMUniversalSyncPreCheckResult instance using the specified properties. + * @param [properties] Properties to set + * @returns PAMUniversalSyncPreCheckResult instance + */ + public static create(properties?: PAM.IPAMUniversalSyncPreCheckResult): PAM.PAMUniversalSyncPreCheckResult; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResult message. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResult.verify|verify} messages. + * @param message PAMUniversalSyncPreCheckResult message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: PAM.IPAMUniversalSyncPreCheckResult, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResult message, length delimited. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResult.verify|verify} messages. + * @param message PAMUniversalSyncPreCheckResult message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: PAM.IPAMUniversalSyncPreCheckResult, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PAMUniversalSyncPreCheckResult message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns PAMUniversalSyncPreCheckResult + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): PAM.PAMUniversalSyncPreCheckResult; + + /** + * Decodes a PAMUniversalSyncPreCheckResult message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns PAMUniversalSyncPreCheckResult + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): PAM.PAMUniversalSyncPreCheckResult; + + /** + * Verifies a PAMUniversalSyncPreCheckResult message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a PAMUniversalSyncPreCheckResult message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns PAMUniversalSyncPreCheckResult + */ + public static fromObject(object: { [k: string]: any }): PAM.PAMUniversalSyncPreCheckResult; + + /** + * Creates a plain object from a PAMUniversalSyncPreCheckResult message. Also converts values to other types if specified. + * @param message PAMUniversalSyncPreCheckResult + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: PAM.PAMUniversalSyncPreCheckResult, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PAMUniversalSyncPreCheckResult to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for PAMUniversalSyncPreCheckResult + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + /** Properties of a PAMUniversalSyncPreCheckResponse. */ + interface IPAMUniversalSyncPreCheckResponse { + + /** PAMUniversalSyncPreCheckResponse results */ + results?: (PAM.IPAMUniversalSyncPreCheckResult[]|null); + } + + /** Represents a PAMUniversalSyncPreCheckResponse. */ + class PAMUniversalSyncPreCheckResponse implements IPAMUniversalSyncPreCheckResponse { + + /** + * Constructs a new PAMUniversalSyncPreCheckResponse. + * @param [properties] Properties to set + */ + constructor(properties?: PAM.IPAMUniversalSyncPreCheckResponse); + + /** PAMUniversalSyncPreCheckResponse results. */ + public results: PAM.IPAMUniversalSyncPreCheckResult[]; + + /** + * Creates a new PAMUniversalSyncPreCheckResponse instance using the specified properties. + * @param [properties] Properties to set + * @returns PAMUniversalSyncPreCheckResponse instance + */ + public static create(properties?: PAM.IPAMUniversalSyncPreCheckResponse): PAM.PAMUniversalSyncPreCheckResponse; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResponse message. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResponse.verify|verify} messages. + * @param message PAMUniversalSyncPreCheckResponse message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: PAM.IPAMUniversalSyncPreCheckResponse, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResponse message, length delimited. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResponse.verify|verify} messages. + * @param message PAMUniversalSyncPreCheckResponse message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: PAM.IPAMUniversalSyncPreCheckResponse, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PAMUniversalSyncPreCheckResponse message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns PAMUniversalSyncPreCheckResponse + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): PAM.PAMUniversalSyncPreCheckResponse; + + /** + * Decodes a PAMUniversalSyncPreCheckResponse message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns PAMUniversalSyncPreCheckResponse + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): PAM.PAMUniversalSyncPreCheckResponse; + + /** + * Verifies a PAMUniversalSyncPreCheckResponse message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a PAMUniversalSyncPreCheckResponse message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns PAMUniversalSyncPreCheckResponse + */ + public static fromObject(object: { [k: string]: any }): PAM.PAMUniversalSyncPreCheckResponse; + + /** + * Creates a plain object from a PAMUniversalSyncPreCheckResponse message. Also converts values to other types if specified. + * @param message PAMUniversalSyncPreCheckResponse + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: PAM.PAMUniversalSyncPreCheckResponse, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PAMUniversalSyncPreCheckResponse to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for PAMUniversalSyncPreCheckResponse + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } } diff --git a/keeperapi/src/proto.js b/keeperapi/src/proto.js index 4228f0d..4bccc26 100644 --- a/keeperapi/src/proto.js +++ b/keeperapi/src/proto.js @@ -4939,6 +4939,424 @@ export const Authentication = $root.Authentication = (() => { return StartLoginRequest; })(); + Authentication.KeysInfo = (function() { + + /** + * Properties of a KeysInfo. + * @memberof Authentication + * @interface IKeysInfo + * @property {Uint8Array|null} [encryptionParams] KeysInfo encryptionParams + * @property {Uint8Array|null} [encryptedDataKey] KeysInfo encryptedDataKey + * @property {number|null} [dataKeyBackupDate] KeysInfo dataKeyBackupDate + * @property {Uint8Array|null} [userAuthUid] KeysInfo userAuthUid + * @property {Uint8Array|null} [encryptedPrivateKey] KeysInfo encryptedPrivateKey + * @property {Uint8Array|null} [encryptedEccPrivateKey] KeysInfo encryptedEccPrivateKey + * @property {Uint8Array|null} [eccPublicKey] KeysInfo eccPublicKey + */ + + /** + * Constructs a new KeysInfo. + * @memberof Authentication + * @classdesc Represents a KeysInfo. + * @implements IKeysInfo + * @constructor + * @param {Authentication.IKeysInfo=} [properties] Properties to set + */ + function KeysInfo(properties) { + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + } + + /** + * KeysInfo encryptionParams. + * @member {Uint8Array} encryptionParams + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.encryptionParams = $util.newBuffer([]); + + /** + * KeysInfo encryptedDataKey. + * @member {Uint8Array} encryptedDataKey + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.encryptedDataKey = $util.newBuffer([]); + + /** + * KeysInfo dataKeyBackupDate. + * @member {number} dataKeyBackupDate + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.dataKeyBackupDate = 0; + + /** + * KeysInfo userAuthUid. + * @member {Uint8Array} userAuthUid + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.userAuthUid = $util.newBuffer([]); + + /** + * KeysInfo encryptedPrivateKey. + * @member {Uint8Array} encryptedPrivateKey + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.encryptedPrivateKey = $util.newBuffer([]); + + /** + * KeysInfo encryptedEccPrivateKey. + * @member {Uint8Array} encryptedEccPrivateKey + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.encryptedEccPrivateKey = $util.newBuffer([]); + + /** + * KeysInfo eccPublicKey. + * @member {Uint8Array} eccPublicKey + * @memberof Authentication.KeysInfo + * @instance + */ + KeysInfo.prototype.eccPublicKey = $util.newBuffer([]); + + /** + * Creates a new KeysInfo instance using the specified properties. + * @function create + * @memberof Authentication.KeysInfo + * @static + * @param {Authentication.IKeysInfo=} [properties] Properties to set + * @returns {Authentication.KeysInfo} KeysInfo instance + */ + KeysInfo.create = function create(properties) { + return new KeysInfo(properties); + }; + + /** + * Encodes the specified KeysInfo message. Does not implicitly {@link Authentication.KeysInfo.verify|verify} messages. + * @function encode + * @memberof Authentication.KeysInfo + * @static + * @param {Authentication.IKeysInfo} message KeysInfo message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + KeysInfo.encode = function encode(message, writer, q) { + if (!writer) + writer = $Writer.create(); + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + if (message.encryptionParams != null && Object.hasOwnProperty.call(message, "encryptionParams")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.encryptionParams); + if (message.encryptedDataKey != null && Object.hasOwnProperty.call(message, "encryptedDataKey")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.encryptedDataKey); + if (message.dataKeyBackupDate != null && Object.hasOwnProperty.call(message, "dataKeyBackupDate")) + writer.uint32(/* id 3, wireType 1 =*/25).double(message.dataKeyBackupDate); + if (message.userAuthUid != null && Object.hasOwnProperty.call(message, "userAuthUid")) + writer.uint32(/* id 4, wireType 2 =*/34).bytes(message.userAuthUid); + if (message.encryptedPrivateKey != null && Object.hasOwnProperty.call(message, "encryptedPrivateKey")) + writer.uint32(/* id 5, wireType 2 =*/42).bytes(message.encryptedPrivateKey); + if (message.encryptedEccPrivateKey != null && Object.hasOwnProperty.call(message, "encryptedEccPrivateKey")) + writer.uint32(/* id 6, wireType 2 =*/50).bytes(message.encryptedEccPrivateKey); + if (message.eccPublicKey != null && Object.hasOwnProperty.call(message, "eccPublicKey")) + writer.uint32(/* id 7, wireType 2 =*/58).bytes(message.eccPublicKey); + return writer; + }; + + /** + * Encodes the specified KeysInfo message, length delimited. Does not implicitly {@link Authentication.KeysInfo.verify|verify} messages. + * @function encodeDelimited + * @memberof Authentication.KeysInfo + * @static + * @param {Authentication.IKeysInfo} message KeysInfo message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + KeysInfo.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a KeysInfo message from the specified reader or buffer. + * @function decode + * @memberof Authentication.KeysInfo + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {Authentication.KeysInfo} KeysInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + KeysInfo.decode = function decode(reader, length, error, long) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (long === undefined) + long = 0; + if (long > $Reader.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.Authentication.KeysInfo(); + while (reader.pos < end) { + let tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.encryptionParams = reader.bytes(); + break; + } + case 2: { + message.encryptedDataKey = reader.bytes(); + break; + } + case 3: { + message.dataKeyBackupDate = reader.double(); + break; + } + case 4: { + message.userAuthUid = reader.bytes(); + break; + } + case 5: { + message.encryptedPrivateKey = reader.bytes(); + break; + } + case 6: { + message.encryptedEccPrivateKey = reader.bytes(); + break; + } + case 7: { + message.eccPublicKey = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7, long); + break; + } + } + return message; + }; + + /** + * Decodes a KeysInfo message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof Authentication.KeysInfo + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {Authentication.KeysInfo} KeysInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + KeysInfo.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a KeysInfo message. + * @function verify + * @memberof Authentication.KeysInfo + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + KeysInfo.verify = function verify(message, long) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + return "maximum nesting depth exceeded"; + if (message.encryptionParams != null && message.hasOwnProperty("encryptionParams")) + if (!(message.encryptionParams && typeof message.encryptionParams.length === "number" || $util.isString(message.encryptionParams))) + return "encryptionParams: buffer expected"; + if (message.encryptedDataKey != null && message.hasOwnProperty("encryptedDataKey")) + if (!(message.encryptedDataKey && typeof message.encryptedDataKey.length === "number" || $util.isString(message.encryptedDataKey))) + return "encryptedDataKey: buffer expected"; + if (message.dataKeyBackupDate != null && message.hasOwnProperty("dataKeyBackupDate")) + if (typeof message.dataKeyBackupDate !== "number") + return "dataKeyBackupDate: number expected"; + if (message.userAuthUid != null && message.hasOwnProperty("userAuthUid")) + if (!(message.userAuthUid && typeof message.userAuthUid.length === "number" || $util.isString(message.userAuthUid))) + return "userAuthUid: buffer expected"; + if (message.encryptedPrivateKey != null && message.hasOwnProperty("encryptedPrivateKey")) + if (!(message.encryptedPrivateKey && typeof message.encryptedPrivateKey.length === "number" || $util.isString(message.encryptedPrivateKey))) + return "encryptedPrivateKey: buffer expected"; + if (message.encryptedEccPrivateKey != null && message.hasOwnProperty("encryptedEccPrivateKey")) + if (!(message.encryptedEccPrivateKey && typeof message.encryptedEccPrivateKey.length === "number" || $util.isString(message.encryptedEccPrivateKey))) + return "encryptedEccPrivateKey: buffer expected"; + if (message.eccPublicKey != null && message.hasOwnProperty("eccPublicKey")) + if (!(message.eccPublicKey && typeof message.eccPublicKey.length === "number" || $util.isString(message.eccPublicKey))) + return "eccPublicKey: buffer expected"; + return null; + }; + + /** + * Creates a KeysInfo message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof Authentication.KeysInfo + * @static + * @param {Object.} object Plain object + * @returns {Authentication.KeysInfo} KeysInfo + */ + KeysInfo.fromObject = function fromObject(object, long) { + if (object instanceof $root.Authentication.KeysInfo) + return object; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let message = new $root.Authentication.KeysInfo(); + if (object.encryptionParams != null) + if (typeof object.encryptionParams === "string") + $util.base64.decode(object.encryptionParams, message.encryptionParams = $util.newBuffer($util.base64.length(object.encryptionParams)), 0); + else if (object.encryptionParams.length >= 0) + message.encryptionParams = object.encryptionParams; + if (object.encryptedDataKey != null) + if (typeof object.encryptedDataKey === "string") + $util.base64.decode(object.encryptedDataKey, message.encryptedDataKey = $util.newBuffer($util.base64.length(object.encryptedDataKey)), 0); + else if (object.encryptedDataKey.length >= 0) + message.encryptedDataKey = object.encryptedDataKey; + if (object.dataKeyBackupDate != null) + message.dataKeyBackupDate = Number(object.dataKeyBackupDate); + if (object.userAuthUid != null) + if (typeof object.userAuthUid === "string") + $util.base64.decode(object.userAuthUid, message.userAuthUid = $util.newBuffer($util.base64.length(object.userAuthUid)), 0); + else if (object.userAuthUid.length >= 0) + message.userAuthUid = object.userAuthUid; + if (object.encryptedPrivateKey != null) + if (typeof object.encryptedPrivateKey === "string") + $util.base64.decode(object.encryptedPrivateKey, message.encryptedPrivateKey = $util.newBuffer($util.base64.length(object.encryptedPrivateKey)), 0); + else if (object.encryptedPrivateKey.length >= 0) + message.encryptedPrivateKey = object.encryptedPrivateKey; + if (object.encryptedEccPrivateKey != null) + if (typeof object.encryptedEccPrivateKey === "string") + $util.base64.decode(object.encryptedEccPrivateKey, message.encryptedEccPrivateKey = $util.newBuffer($util.base64.length(object.encryptedEccPrivateKey)), 0); + else if (object.encryptedEccPrivateKey.length >= 0) + message.encryptedEccPrivateKey = object.encryptedEccPrivateKey; + if (object.eccPublicKey != null) + if (typeof object.eccPublicKey === "string") + $util.base64.decode(object.eccPublicKey, message.eccPublicKey = $util.newBuffer($util.base64.length(object.eccPublicKey)), 0); + else if (object.eccPublicKey.length >= 0) + message.eccPublicKey = object.eccPublicKey; + return message; + }; + + /** + * Creates a plain object from a KeysInfo message. Also converts values to other types if specified. + * @function toObject + * @memberof Authentication.KeysInfo + * @static + * @param {Authentication.KeysInfo} message KeysInfo + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + KeysInfo.toObject = function toObject(message, options, q) { + if (!options) + options = {}; + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + if (options.bytes === String) + object.encryptionParams = ""; + else { + object.encryptionParams = []; + if (options.bytes !== Array) + object.encryptionParams = $util.newBuffer(object.encryptionParams); + } + if (options.bytes === String) + object.encryptedDataKey = ""; + else { + object.encryptedDataKey = []; + if (options.bytes !== Array) + object.encryptedDataKey = $util.newBuffer(object.encryptedDataKey); + } + object.dataKeyBackupDate = 0; + if (options.bytes === String) + object.userAuthUid = ""; + else { + object.userAuthUid = []; + if (options.bytes !== Array) + object.userAuthUid = $util.newBuffer(object.userAuthUid); + } + if (options.bytes === String) + object.encryptedPrivateKey = ""; + else { + object.encryptedPrivateKey = []; + if (options.bytes !== Array) + object.encryptedPrivateKey = $util.newBuffer(object.encryptedPrivateKey); + } + if (options.bytes === String) + object.encryptedEccPrivateKey = ""; + else { + object.encryptedEccPrivateKey = []; + if (options.bytes !== Array) + object.encryptedEccPrivateKey = $util.newBuffer(object.encryptedEccPrivateKey); + } + if (options.bytes === String) + object.eccPublicKey = ""; + else { + object.eccPublicKey = []; + if (options.bytes !== Array) + object.eccPublicKey = $util.newBuffer(object.eccPublicKey); + } + } + if (message.encryptionParams != null && message.hasOwnProperty("encryptionParams")) + object.encryptionParams = options.bytes === String ? $util.base64.encode(message.encryptionParams, 0, message.encryptionParams.length) : options.bytes === Array ? Array.prototype.slice.call(message.encryptionParams) : message.encryptionParams; + if (message.encryptedDataKey != null && message.hasOwnProperty("encryptedDataKey")) + object.encryptedDataKey = options.bytes === String ? $util.base64.encode(message.encryptedDataKey, 0, message.encryptedDataKey.length) : options.bytes === Array ? Array.prototype.slice.call(message.encryptedDataKey) : message.encryptedDataKey; + if (message.dataKeyBackupDate != null && message.hasOwnProperty("dataKeyBackupDate")) + object.dataKeyBackupDate = options.json && !isFinite(message.dataKeyBackupDate) ? String(message.dataKeyBackupDate) : message.dataKeyBackupDate; + if (message.userAuthUid != null && message.hasOwnProperty("userAuthUid")) + object.userAuthUid = options.bytes === String ? $util.base64.encode(message.userAuthUid, 0, message.userAuthUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.userAuthUid) : message.userAuthUid; + if (message.encryptedPrivateKey != null && message.hasOwnProperty("encryptedPrivateKey")) + object.encryptedPrivateKey = options.bytes === String ? $util.base64.encode(message.encryptedPrivateKey, 0, message.encryptedPrivateKey.length) : options.bytes === Array ? Array.prototype.slice.call(message.encryptedPrivateKey) : message.encryptedPrivateKey; + if (message.encryptedEccPrivateKey != null && message.hasOwnProperty("encryptedEccPrivateKey")) + object.encryptedEccPrivateKey = options.bytes === String ? $util.base64.encode(message.encryptedEccPrivateKey, 0, message.encryptedEccPrivateKey.length) : options.bytes === Array ? Array.prototype.slice.call(message.encryptedEccPrivateKey) : message.encryptedEccPrivateKey; + if (message.eccPublicKey != null && message.hasOwnProperty("eccPublicKey")) + object.eccPublicKey = options.bytes === String ? $util.base64.encode(message.eccPublicKey, 0, message.eccPublicKey.length) : options.bytes === Array ? Array.prototype.slice.call(message.eccPublicKey) : message.eccPublicKey; + return object; + }; + + /** + * Converts this KeysInfo to JSON. + * @function toJSON + * @memberof Authentication.KeysInfo + * @instance + * @returns {Object.} JSON object + */ + KeysInfo.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for KeysInfo + * @function getTypeUrl + * @memberof Authentication.KeysInfo + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + KeysInfo.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/Authentication.KeysInfo"; + }; + + return KeysInfo; + })(); + Authentication.LoginResponse = (function() { /** @@ -4961,6 +5379,8 @@ export const Authentication = $root.Authentication = (() => { * @property {string|null} [stateSpecificValue] LoginResponse stateSpecificValue * @property {string|null} [ssoClientVersion] LoginResponse ssoClientVersion * @property {string|null} [sessionTokenTypeModifier] LoginResponse sessionTokenTypeModifier + * @property {Authentication.IKeysInfo|null} [keysInfo] LoginResponse keysInfo + * @property {Uint8Array|null} [clientKey] LoginResponse clientKey */ /** @@ -5108,6 +5528,22 @@ export const Authentication = $root.Authentication = (() => { */ LoginResponse.prototype.sessionTokenTypeModifier = ""; + /** + * LoginResponse keysInfo. + * @member {Authentication.IKeysInfo|null|undefined} keysInfo + * @memberof Authentication.LoginResponse + * @instance + */ + LoginResponse.prototype.keysInfo = null; + + /** + * LoginResponse clientKey. + * @member {Uint8Array} clientKey + * @memberof Authentication.LoginResponse + * @instance + */ + LoginResponse.prototype.clientKey = $util.newBuffer([]); + /** * Creates a new LoginResponse instance using the specified properties. * @function create @@ -5170,6 +5606,10 @@ export const Authentication = $root.Authentication = (() => { writer.uint32(/* id 15, wireType 2 =*/122).string(message.ssoClientVersion); if (message.sessionTokenTypeModifier != null && Object.hasOwnProperty.call(message, "sessionTokenTypeModifier")) writer.uint32(/* id 16, wireType 2 =*/130).string(message.sessionTokenTypeModifier); + if (message.keysInfo != null && Object.hasOwnProperty.call(message, "keysInfo")) + $root.Authentication.KeysInfo.encode(message.keysInfo, writer.uint32(/* id 17, wireType 2 =*/138).fork(), q + 1).ldelim(); + if (message.clientKey != null && Object.hasOwnProperty.call(message, "clientKey")) + writer.uint32(/* id 18, wireType 2 =*/146).bytes(message.clientKey); return writer; }; @@ -5278,6 +5718,14 @@ export const Authentication = $root.Authentication = (() => { message.sessionTokenTypeModifier = reader.string(); break; } + case 17: { + message.keysInfo = $root.Authentication.KeysInfo.decode(reader, reader.uint32(), undefined, long + 1); + break; + } + case 18: { + message.clientKey = reader.bytes(); + break; + } default: reader.skipType(tag & 7, long); break; @@ -5426,6 +5874,14 @@ export const Authentication = $root.Authentication = (() => { if (message.sessionTokenTypeModifier != null && message.hasOwnProperty("sessionTokenTypeModifier")) if (!$util.isString(message.sessionTokenTypeModifier)) return "sessionTokenTypeModifier: string expected"; + if (message.keysInfo != null && message.hasOwnProperty("keysInfo")) { + let error = $root.Authentication.KeysInfo.verify(message.keysInfo, long + 1); + if (error) + return "keysInfo." + error; + } + if (message.clientKey != null && message.hasOwnProperty("clientKey")) + if (!(message.clientKey && typeof message.clientKey.length === "number" || $util.isString(message.clientKey))) + return "clientKey: buffer expected"; return null; }; @@ -5686,6 +6142,16 @@ export const Authentication = $root.Authentication = (() => { message.ssoClientVersion = String(object.ssoClientVersion); if (object.sessionTokenTypeModifier != null) message.sessionTokenTypeModifier = String(object.sessionTokenTypeModifier); + if (object.keysInfo != null) { + if (typeof object.keysInfo !== "object") + throw TypeError(".Authentication.LoginResponse.keysInfo: object expected"); + message.keysInfo = $root.Authentication.KeysInfo.fromObject(object.keysInfo, long + 1); + } + if (object.clientKey != null) + if (typeof object.clientKey === "string") + $util.base64.decode(object.clientKey, message.clientKey = $util.newBuffer($util.base64.length(object.clientKey)), 0); + else if (object.clientKey.length >= 0) + message.clientKey = object.clientKey; return message; }; @@ -5755,6 +6221,14 @@ export const Authentication = $root.Authentication = (() => { object.stateSpecificValue = ""; object.ssoClientVersion = ""; object.sessionTokenTypeModifier = ""; + object.keysInfo = null; + if (options.bytes === String) + object.clientKey = ""; + else { + object.clientKey = []; + if (options.bytes !== Array) + object.clientKey = $util.newBuffer(object.clientKey); + } } if (message.loginState != null && message.hasOwnProperty("loginState")) object.loginState = options.enums === String ? $root.Authentication.LoginState[message.loginState] === undefined ? message.loginState : $root.Authentication.LoginState[message.loginState] : message.loginState; @@ -5794,6 +6268,10 @@ export const Authentication = $root.Authentication = (() => { object.ssoClientVersion = message.ssoClientVersion; if (message.sessionTokenTypeModifier != null && message.hasOwnProperty("sessionTokenTypeModifier")) object.sessionTokenTypeModifier = message.sessionTokenTypeModifier; + if (message.keysInfo != null && message.hasOwnProperty("keysInfo")) + object.keysInfo = $root.Authentication.KeysInfo.toObject(message.keysInfo, options, q + 1); + if (message.clientKey != null && message.hasOwnProperty("clientKey")) + object.clientKey = options.bytes === String ? $util.base64.encode(message.clientKey, 0, message.clientKey.length) : options.bytes === Array ? Array.prototype.slice.call(message.clientKey) : message.clientKey; return object; }; @@ -255936,6 +256414,997 @@ export const record = $root.record = (() => { return sharing; })(); + v3.RecordsAddRequest = (function() { + + /** + * Properties of a RecordsAddRequest. + * @memberof record.v3 + * @interface IRecordsAddRequest + * @property {Array.|null} [records] RecordsAddRequest records + * @property {number|null} [clientTime] RecordsAddRequest clientTime + * @property {Records.RecordKeyType|null} [securityDataKeyType] RecordsAddRequest securityDataKeyType + */ + + /** + * Constructs a new RecordsAddRequest. + * @memberof record.v3 + * @classdesc Represents a RecordsAddRequest. + * @implements IRecordsAddRequest + * @constructor + * @param {record.v3.IRecordsAddRequest=} [properties] Properties to set + */ + function RecordsAddRequest(properties) { + this.records = []; + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + } + + /** + * RecordsAddRequest records. + * @member {Array.} records + * @memberof record.v3.RecordsAddRequest + * @instance + */ + RecordsAddRequest.prototype.records = $util.emptyArray; + + /** + * RecordsAddRequest clientTime. + * @member {number} clientTime + * @memberof record.v3.RecordsAddRequest + * @instance + */ + RecordsAddRequest.prototype.clientTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0; + + /** + * RecordsAddRequest securityDataKeyType. + * @member {Records.RecordKeyType} securityDataKeyType + * @memberof record.v3.RecordsAddRequest + * @instance + */ + RecordsAddRequest.prototype.securityDataKeyType = 0; + + /** + * Creates a new RecordsAddRequest instance using the specified properties. + * @function create + * @memberof record.v3.RecordsAddRequest + * @static + * @param {record.v3.IRecordsAddRequest=} [properties] Properties to set + * @returns {record.v3.RecordsAddRequest} RecordsAddRequest instance + */ + RecordsAddRequest.create = function create(properties) { + return new RecordsAddRequest(properties); + }; + + /** + * Encodes the specified RecordsAddRequest message. Does not implicitly {@link record.v3.RecordsAddRequest.verify|verify} messages. + * @function encode + * @memberof record.v3.RecordsAddRequest + * @static + * @param {record.v3.IRecordsAddRequest} message RecordsAddRequest message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RecordsAddRequest.encode = function encode(message, writer, q) { + if (!writer) + writer = $Writer.create(); + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + if (message.records != null && message.records.length) + for (let i = 0; i < message.records.length; ++i) + $root.record.v3.RecordAdd.encode(message.records[i], writer.uint32(/* id 1, wireType 2 =*/10).fork(), q + 1).ldelim(); + if (message.clientTime != null && Object.hasOwnProperty.call(message, "clientTime")) + writer.uint32(/* id 2, wireType 0 =*/16).int64(message.clientTime); + if (message.securityDataKeyType != null && Object.hasOwnProperty.call(message, "securityDataKeyType")) + writer.uint32(/* id 3, wireType 0 =*/24).int32(message.securityDataKeyType); + return writer; + }; + + /** + * Encodes the specified RecordsAddRequest message, length delimited. Does not implicitly {@link record.v3.RecordsAddRequest.verify|verify} messages. + * @function encodeDelimited + * @memberof record.v3.RecordsAddRequest + * @static + * @param {record.v3.IRecordsAddRequest} message RecordsAddRequest message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RecordsAddRequest.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a RecordsAddRequest message from the specified reader or buffer. + * @function decode + * @memberof record.v3.RecordsAddRequest + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {record.v3.RecordsAddRequest} RecordsAddRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RecordsAddRequest.decode = function decode(reader, length, error, long) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (long === undefined) + long = 0; + if (long > $Reader.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.record.v3.RecordsAddRequest(); + while (reader.pos < end) { + let tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + if (!(message.records && message.records.length)) + message.records = []; + message.records.push($root.record.v3.RecordAdd.decode(reader, reader.uint32(), undefined, long + 1)); + break; + } + case 2: { + message.clientTime = reader.int64(); + break; + } + case 3: { + message.securityDataKeyType = reader.int32(); + break; + } + default: + reader.skipType(tag & 7, long); + break; + } + } + return message; + }; + + /** + * Decodes a RecordsAddRequest message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof record.v3.RecordsAddRequest + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {record.v3.RecordsAddRequest} RecordsAddRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RecordsAddRequest.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a RecordsAddRequest message. + * @function verify + * @memberof record.v3.RecordsAddRequest + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + RecordsAddRequest.verify = function verify(message, long) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + return "maximum nesting depth exceeded"; + if (message.records != null && message.hasOwnProperty("records")) { + if (!Array.isArray(message.records)) + return "records: array expected"; + for (let i = 0; i < message.records.length; ++i) { + let error = $root.record.v3.RecordAdd.verify(message.records[i], long + 1); + if (error) + return "records." + error; + } + } + if (message.clientTime != null && message.hasOwnProperty("clientTime")) + if (!$util.isInteger(message.clientTime) && !(message.clientTime && $util.isInteger(message.clientTime.low) && $util.isInteger(message.clientTime.high))) + return "clientTime: integer|Long expected"; + if (message.securityDataKeyType != null && message.hasOwnProperty("securityDataKeyType")) + switch (message.securityDataKeyType) { + default: + return "securityDataKeyType: enum value expected"; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + break; + } + return null; + }; + + /** + * Creates a RecordsAddRequest message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof record.v3.RecordsAddRequest + * @static + * @param {Object.} object Plain object + * @returns {record.v3.RecordsAddRequest} RecordsAddRequest + */ + RecordsAddRequest.fromObject = function fromObject(object, long) { + if (object instanceof $root.record.v3.RecordsAddRequest) + return object; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let message = new $root.record.v3.RecordsAddRequest(); + if (object.records) { + if (!Array.isArray(object.records)) + throw TypeError(".record.v3.RecordsAddRequest.records: array expected"); + message.records = []; + for (let i = 0; i < object.records.length; ++i) { + if (typeof object.records[i] !== "object") + throw TypeError(".record.v3.RecordsAddRequest.records: object expected"); + message.records[i] = $root.record.v3.RecordAdd.fromObject(object.records[i], long + 1); + } + } + if (object.clientTime != null) + if ($util.Long) + message.clientTime = $util.Long.fromValue(object.clientTime, false); + else if (typeof object.clientTime === "string") + message.clientTime = parseInt(object.clientTime, 10); + else if (typeof object.clientTime === "number") + message.clientTime = object.clientTime; + else if (typeof object.clientTime === "object") + message.clientTime = new $util.LongBits(object.clientTime.low >>> 0, object.clientTime.high >>> 0).toNumber(); + switch (object.securityDataKeyType) { + default: + if (typeof object.securityDataKeyType === "number") { + message.securityDataKeyType = object.securityDataKeyType; + break; + } + break; + case "NO_KEY": + case 0: + message.securityDataKeyType = 0; + break; + case "ENCRYPTED_BY_DATA_KEY": + case 1: + message.securityDataKeyType = 1; + break; + case "ENCRYPTED_BY_PUBLIC_KEY": + case 2: + message.securityDataKeyType = 2; + break; + case "ENCRYPTED_BY_DATA_KEY_GCM": + case 3: + message.securityDataKeyType = 3; + break; + case "ENCRYPTED_BY_PUBLIC_KEY_ECC": + case 4: + message.securityDataKeyType = 4; + break; + case "ENCRYPTED_BY_ROOT_KEY_CBC": + case 5: + message.securityDataKeyType = 5; + break; + case "ENCRYPTED_BY_ROOT_KEY_GCM": + case 6: + message.securityDataKeyType = 6; + break; + } + return message; + }; + + /** + * Creates a plain object from a RecordsAddRequest message. Also converts values to other types if specified. + * @function toObject + * @memberof record.v3.RecordsAddRequest + * @static + * @param {record.v3.RecordsAddRequest} message RecordsAddRequest + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + RecordsAddRequest.toObject = function toObject(message, options, q) { + if (!options) + options = {}; + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.records = []; + if (options.defaults) { + if ($util.Long) { + let long = new $util.Long(0, 0, false); + object.clientTime = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : typeof BigInt !== "undefined" && options.longs === BigInt ? long.toBigInt() : long; + } else + object.clientTime = options.longs === String ? "0" : typeof BigInt !== "undefined" && options.longs === BigInt ? BigInt("0") : 0; + object.securityDataKeyType = options.enums === String ? "NO_KEY" : 0; + } + if (message.records && message.records.length) { + object.records = []; + for (let j = 0; j < message.records.length; ++j) + object.records[j] = $root.record.v3.RecordAdd.toObject(message.records[j], options, q + 1); + } + if (message.clientTime != null && message.hasOwnProperty("clientTime")) + if (typeof BigInt !== "undefined" && options.longs === BigInt) + object.clientTime = typeof message.clientTime === "number" ? BigInt(message.clientTime) : $util.Long.fromBits(message.clientTime.low >>> 0, message.clientTime.high >>> 0, false).toBigInt(); + else if (typeof message.clientTime === "number") + object.clientTime = options.longs === String ? String(message.clientTime) : message.clientTime; + else + object.clientTime = options.longs === String ? $util.Long.prototype.toString.call(message.clientTime) : options.longs === Number ? new $util.LongBits(message.clientTime.low >>> 0, message.clientTime.high >>> 0).toNumber() : message.clientTime; + if (message.securityDataKeyType != null && message.hasOwnProperty("securityDataKeyType")) + object.securityDataKeyType = options.enums === String ? $root.Records.RecordKeyType[message.securityDataKeyType] === undefined ? message.securityDataKeyType : $root.Records.RecordKeyType[message.securityDataKeyType] : message.securityDataKeyType; + return object; + }; + + /** + * Converts this RecordsAddRequest to JSON. + * @function toJSON + * @memberof record.v3.RecordsAddRequest + * @instance + * @returns {Object.} JSON object + */ + RecordsAddRequest.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for RecordsAddRequest + * @function getTypeUrl + * @memberof record.v3.RecordsAddRequest + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + RecordsAddRequest.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/record.v3.RecordsAddRequest"; + }; + + return RecordsAddRequest; + })(); + + v3.RecordAdd = (function() { + + /** + * Properties of a RecordAdd. + * @memberof record.v3 + * @interface IRecordAdd + * @property {Uint8Array|null} [recordUid] RecordAdd recordUid + * @property {Uint8Array|null} [recordKey] RecordAdd recordKey + * @property {Folder.EncryptedKeyType|null} [recordKeyType] RecordAdd recordKeyType + * @property {Folder.FolderKeyEncryptionType|null} [recordKeyEncryptedBy] Record creates in root folder is encrypted by user key. + * Record creates in non-root folder is encrypted by folder key. + * @property {number|null} [clientModifiedTime] RecordAdd clientModifiedTime + * @property {Uint8Array|null} [data] RecordAdd data + * @property {Uint8Array|null} [nonSharedData] RecordAdd nonSharedData + * @property {Uint8Array|null} [folderUid] RecordAdd folderUid + * @property {Array.|null} [recordLinks] RecordAdd recordLinks + * @property {Records.IRecordAudit|null} [audit] RecordAdd audit + * @property {Records.ISecurityData|null} [securityData] RecordAdd securityData + * @property {Records.ISecurityScoreData|null} [securityScoreData] RecordAdd securityScoreData + */ + + /** + * Constructs a new RecordAdd. + * @memberof record.v3 + * @classdesc Represents a RecordAdd. + * @implements IRecordAdd + * @constructor + * @param {record.v3.IRecordAdd=} [properties] Properties to set + */ + function RecordAdd(properties) { + this.recordLinks = []; + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + } + + /** + * RecordAdd recordUid. + * @member {Uint8Array} recordUid + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.recordUid = $util.newBuffer([]); + + /** + * RecordAdd recordKey. + * @member {Uint8Array} recordKey + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.recordKey = $util.newBuffer([]); + + /** + * RecordAdd recordKeyType. + * @member {Folder.EncryptedKeyType} recordKeyType + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.recordKeyType = 0; + + /** + * Record creates in root folder is encrypted by user key. + * Record creates in non-root folder is encrypted by folder key. + * @member {Folder.FolderKeyEncryptionType} recordKeyEncryptedBy + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.recordKeyEncryptedBy = 0; + + /** + * RecordAdd clientModifiedTime. + * @member {number} clientModifiedTime + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.clientModifiedTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0; + + /** + * RecordAdd data. + * @member {Uint8Array} data + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.data = $util.newBuffer([]); + + /** + * RecordAdd nonSharedData. + * @member {Uint8Array} nonSharedData + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.nonSharedData = $util.newBuffer([]); + + /** + * RecordAdd folderUid. + * @member {Uint8Array} folderUid + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.folderUid = $util.newBuffer([]); + + /** + * RecordAdd recordLinks. + * @member {Array.} recordLinks + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.recordLinks = $util.emptyArray; + + /** + * RecordAdd audit. + * @member {Records.IRecordAudit|null|undefined} audit + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.audit = null; + + /** + * RecordAdd securityData. + * @member {Records.ISecurityData|null|undefined} securityData + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.securityData = null; + + /** + * RecordAdd securityScoreData. + * @member {Records.ISecurityScoreData|null|undefined} securityScoreData + * @memberof record.v3.RecordAdd + * @instance + */ + RecordAdd.prototype.securityScoreData = null; + + /** + * Creates a new RecordAdd instance using the specified properties. + * @function create + * @memberof record.v3.RecordAdd + * @static + * @param {record.v3.IRecordAdd=} [properties] Properties to set + * @returns {record.v3.RecordAdd} RecordAdd instance + */ + RecordAdd.create = function create(properties) { + return new RecordAdd(properties); + }; + + /** + * Encodes the specified RecordAdd message. Does not implicitly {@link record.v3.RecordAdd.verify|verify} messages. + * @function encode + * @memberof record.v3.RecordAdd + * @static + * @param {record.v3.IRecordAdd} message RecordAdd message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RecordAdd.encode = function encode(message, writer, q) { + if (!writer) + writer = $Writer.create(); + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + if (message.recordUid != null && Object.hasOwnProperty.call(message, "recordUid")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.recordUid); + if (message.recordKey != null && Object.hasOwnProperty.call(message, "recordKey")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.recordKey); + if (message.recordKeyType != null && Object.hasOwnProperty.call(message, "recordKeyType")) + writer.uint32(/* id 3, wireType 0 =*/24).int32(message.recordKeyType); + if (message.recordKeyEncryptedBy != null && Object.hasOwnProperty.call(message, "recordKeyEncryptedBy")) + writer.uint32(/* id 4, wireType 0 =*/32).int32(message.recordKeyEncryptedBy); + if (message.clientModifiedTime != null && Object.hasOwnProperty.call(message, "clientModifiedTime")) + writer.uint32(/* id 5, wireType 0 =*/40).int64(message.clientModifiedTime); + if (message.data != null && Object.hasOwnProperty.call(message, "data")) + writer.uint32(/* id 6, wireType 2 =*/50).bytes(message.data); + if (message.nonSharedData != null && Object.hasOwnProperty.call(message, "nonSharedData")) + writer.uint32(/* id 7, wireType 2 =*/58).bytes(message.nonSharedData); + if (message.folderUid != null && Object.hasOwnProperty.call(message, "folderUid")) + writer.uint32(/* id 8, wireType 2 =*/66).bytes(message.folderUid); + if (message.recordLinks != null && message.recordLinks.length) + for (let i = 0; i < message.recordLinks.length; ++i) + $root.Records.RecordLink.encode(message.recordLinks[i], writer.uint32(/* id 9, wireType 2 =*/74).fork(), q + 1).ldelim(); + if (message.audit != null && Object.hasOwnProperty.call(message, "audit")) + $root.Records.RecordAudit.encode(message.audit, writer.uint32(/* id 10, wireType 2 =*/82).fork(), q + 1).ldelim(); + if (message.securityData != null && Object.hasOwnProperty.call(message, "securityData")) + $root.Records.SecurityData.encode(message.securityData, writer.uint32(/* id 11, wireType 2 =*/90).fork(), q + 1).ldelim(); + if (message.securityScoreData != null && Object.hasOwnProperty.call(message, "securityScoreData")) + $root.Records.SecurityScoreData.encode(message.securityScoreData, writer.uint32(/* id 12, wireType 2 =*/98).fork(), q + 1).ldelim(); + return writer; + }; + + /** + * Encodes the specified RecordAdd message, length delimited. Does not implicitly {@link record.v3.RecordAdd.verify|verify} messages. + * @function encodeDelimited + * @memberof record.v3.RecordAdd + * @static + * @param {record.v3.IRecordAdd} message RecordAdd message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + RecordAdd.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a RecordAdd message from the specified reader or buffer. + * @function decode + * @memberof record.v3.RecordAdd + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {record.v3.RecordAdd} RecordAdd + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RecordAdd.decode = function decode(reader, length, error, long) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (long === undefined) + long = 0; + if (long > $Reader.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.record.v3.RecordAdd(); + while (reader.pos < end) { + let tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.recordUid = reader.bytes(); + break; + } + case 2: { + message.recordKey = reader.bytes(); + break; + } + case 3: { + message.recordKeyType = reader.int32(); + break; + } + case 4: { + message.recordKeyEncryptedBy = reader.int32(); + break; + } + case 5: { + message.clientModifiedTime = reader.int64(); + break; + } + case 6: { + message.data = reader.bytes(); + break; + } + case 7: { + message.nonSharedData = reader.bytes(); + break; + } + case 8: { + message.folderUid = reader.bytes(); + break; + } + case 9: { + if (!(message.recordLinks && message.recordLinks.length)) + message.recordLinks = []; + message.recordLinks.push($root.Records.RecordLink.decode(reader, reader.uint32(), undefined, long + 1)); + break; + } + case 10: { + message.audit = $root.Records.RecordAudit.decode(reader, reader.uint32(), undefined, long + 1); + break; + } + case 11: { + message.securityData = $root.Records.SecurityData.decode(reader, reader.uint32(), undefined, long + 1); + break; + } + case 12: { + message.securityScoreData = $root.Records.SecurityScoreData.decode(reader, reader.uint32(), undefined, long + 1); + break; + } + default: + reader.skipType(tag & 7, long); + break; + } + } + return message; + }; + + /** + * Decodes a RecordAdd message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof record.v3.RecordAdd + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {record.v3.RecordAdd} RecordAdd + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + RecordAdd.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a RecordAdd message. + * @function verify + * @memberof record.v3.RecordAdd + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + RecordAdd.verify = function verify(message, long) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + return "maximum nesting depth exceeded"; + if (message.recordUid != null && message.hasOwnProperty("recordUid")) + if (!(message.recordUid && typeof message.recordUid.length === "number" || $util.isString(message.recordUid))) + return "recordUid: buffer expected"; + if (message.recordKey != null && message.hasOwnProperty("recordKey")) + if (!(message.recordKey && typeof message.recordKey.length === "number" || $util.isString(message.recordKey))) + return "recordKey: buffer expected"; + if (message.recordKeyType != null && message.hasOwnProperty("recordKeyType")) + switch (message.recordKeyType) { + default: + return "recordKeyType: enum value expected"; + case 0: + case 1: + case 2: + case 3: + case 4: + break; + } + if (message.recordKeyEncryptedBy != null && message.hasOwnProperty("recordKeyEncryptedBy")) + switch (message.recordKeyEncryptedBy) { + default: + return "recordKeyEncryptedBy: enum value expected"; + case 0: + case 1: + case 2: + break; + } + if (message.clientModifiedTime != null && message.hasOwnProperty("clientModifiedTime")) + if (!$util.isInteger(message.clientModifiedTime) && !(message.clientModifiedTime && $util.isInteger(message.clientModifiedTime.low) && $util.isInteger(message.clientModifiedTime.high))) + return "clientModifiedTime: integer|Long expected"; + if (message.data != null && message.hasOwnProperty("data")) + if (!(message.data && typeof message.data.length === "number" || $util.isString(message.data))) + return "data: buffer expected"; + if (message.nonSharedData != null && message.hasOwnProperty("nonSharedData")) + if (!(message.nonSharedData && typeof message.nonSharedData.length === "number" || $util.isString(message.nonSharedData))) + return "nonSharedData: buffer expected"; + if (message.folderUid != null && message.hasOwnProperty("folderUid")) + if (!(message.folderUid && typeof message.folderUid.length === "number" || $util.isString(message.folderUid))) + return "folderUid: buffer expected"; + if (message.recordLinks != null && message.hasOwnProperty("recordLinks")) { + if (!Array.isArray(message.recordLinks)) + return "recordLinks: array expected"; + for (let i = 0; i < message.recordLinks.length; ++i) { + let error = $root.Records.RecordLink.verify(message.recordLinks[i], long + 1); + if (error) + return "recordLinks." + error; + } + } + if (message.audit != null && message.hasOwnProperty("audit")) { + let error = $root.Records.RecordAudit.verify(message.audit, long + 1); + if (error) + return "audit." + error; + } + if (message.securityData != null && message.hasOwnProperty("securityData")) { + let error = $root.Records.SecurityData.verify(message.securityData, long + 1); + if (error) + return "securityData." + error; + } + if (message.securityScoreData != null && message.hasOwnProperty("securityScoreData")) { + let error = $root.Records.SecurityScoreData.verify(message.securityScoreData, long + 1); + if (error) + return "securityScoreData." + error; + } + return null; + }; + + /** + * Creates a RecordAdd message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof record.v3.RecordAdd + * @static + * @param {Object.} object Plain object + * @returns {record.v3.RecordAdd} RecordAdd + */ + RecordAdd.fromObject = function fromObject(object, long) { + if (object instanceof $root.record.v3.RecordAdd) + return object; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let message = new $root.record.v3.RecordAdd(); + if (object.recordUid != null) + if (typeof object.recordUid === "string") + $util.base64.decode(object.recordUid, message.recordUid = $util.newBuffer($util.base64.length(object.recordUid)), 0); + else if (object.recordUid.length >= 0) + message.recordUid = object.recordUid; + if (object.recordKey != null) + if (typeof object.recordKey === "string") + $util.base64.decode(object.recordKey, message.recordKey = $util.newBuffer($util.base64.length(object.recordKey)), 0); + else if (object.recordKey.length >= 0) + message.recordKey = object.recordKey; + switch (object.recordKeyType) { + default: + if (typeof object.recordKeyType === "number") { + message.recordKeyType = object.recordKeyType; + break; + } + break; + case "no_key": + case 0: + message.recordKeyType = 0; + break; + case "encrypted_by_data_key": + case 1: + message.recordKeyType = 1; + break; + case "encrypted_by_public_key": + case 2: + message.recordKeyType = 2; + break; + case "encrypted_by_data_key_gcm": + case 3: + message.recordKeyType = 3; + break; + case "encrypted_by_public_key_ecc": + case 4: + message.recordKeyType = 4; + break; + } + switch (object.recordKeyEncryptedBy) { + default: + if (typeof object.recordKeyEncryptedBy === "number") { + message.recordKeyEncryptedBy = object.recordKeyEncryptedBy; + break; + } + break; + case "ENCRYPTED_BY_USER_KEY": + case 0: + message.recordKeyEncryptedBy = 0; + break; + case "ENCRYPTED_BY_PARENT_KEY": + case 1: + message.recordKeyEncryptedBy = 1; + break; + case "ENCRYPTED_BY_TEAM_KEY": + case 2: + message.recordKeyEncryptedBy = 2; + break; + } + if (object.clientModifiedTime != null) + if ($util.Long) + message.clientModifiedTime = $util.Long.fromValue(object.clientModifiedTime, false); + else if (typeof object.clientModifiedTime === "string") + message.clientModifiedTime = parseInt(object.clientModifiedTime, 10); + else if (typeof object.clientModifiedTime === "number") + message.clientModifiedTime = object.clientModifiedTime; + else if (typeof object.clientModifiedTime === "object") + message.clientModifiedTime = new $util.LongBits(object.clientModifiedTime.low >>> 0, object.clientModifiedTime.high >>> 0).toNumber(); + if (object.data != null) + if (typeof object.data === "string") + $util.base64.decode(object.data, message.data = $util.newBuffer($util.base64.length(object.data)), 0); + else if (object.data.length >= 0) + message.data = object.data; + if (object.nonSharedData != null) + if (typeof object.nonSharedData === "string") + $util.base64.decode(object.nonSharedData, message.nonSharedData = $util.newBuffer($util.base64.length(object.nonSharedData)), 0); + else if (object.nonSharedData.length >= 0) + message.nonSharedData = object.nonSharedData; + if (object.folderUid != null) + if (typeof object.folderUid === "string") + $util.base64.decode(object.folderUid, message.folderUid = $util.newBuffer($util.base64.length(object.folderUid)), 0); + else if (object.folderUid.length >= 0) + message.folderUid = object.folderUid; + if (object.recordLinks) { + if (!Array.isArray(object.recordLinks)) + throw TypeError(".record.v3.RecordAdd.recordLinks: array expected"); + message.recordLinks = []; + for (let i = 0; i < object.recordLinks.length; ++i) { + if (typeof object.recordLinks[i] !== "object") + throw TypeError(".record.v3.RecordAdd.recordLinks: object expected"); + message.recordLinks[i] = $root.Records.RecordLink.fromObject(object.recordLinks[i], long + 1); + } + } + if (object.audit != null) { + if (typeof object.audit !== "object") + throw TypeError(".record.v3.RecordAdd.audit: object expected"); + message.audit = $root.Records.RecordAudit.fromObject(object.audit, long + 1); + } + if (object.securityData != null) { + if (typeof object.securityData !== "object") + throw TypeError(".record.v3.RecordAdd.securityData: object expected"); + message.securityData = $root.Records.SecurityData.fromObject(object.securityData, long + 1); + } + if (object.securityScoreData != null) { + if (typeof object.securityScoreData !== "object") + throw TypeError(".record.v3.RecordAdd.securityScoreData: object expected"); + message.securityScoreData = $root.Records.SecurityScoreData.fromObject(object.securityScoreData, long + 1); + } + return message; + }; + + /** + * Creates a plain object from a RecordAdd message. Also converts values to other types if specified. + * @function toObject + * @memberof record.v3.RecordAdd + * @static + * @param {record.v3.RecordAdd} message RecordAdd + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + RecordAdd.toObject = function toObject(message, options, q) { + if (!options) + options = {}; + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.recordLinks = []; + if (options.defaults) { + if (options.bytes === String) + object.recordUid = ""; + else { + object.recordUid = []; + if (options.bytes !== Array) + object.recordUid = $util.newBuffer(object.recordUid); + } + if (options.bytes === String) + object.recordKey = ""; + else { + object.recordKey = []; + if (options.bytes !== Array) + object.recordKey = $util.newBuffer(object.recordKey); + } + object.recordKeyType = options.enums === String ? "no_key" : 0; + object.recordKeyEncryptedBy = options.enums === String ? "ENCRYPTED_BY_USER_KEY" : 0; + if ($util.Long) { + let long = new $util.Long(0, 0, false); + object.clientModifiedTime = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : typeof BigInt !== "undefined" && options.longs === BigInt ? long.toBigInt() : long; + } else + object.clientModifiedTime = options.longs === String ? "0" : typeof BigInt !== "undefined" && options.longs === BigInt ? BigInt("0") : 0; + if (options.bytes === String) + object.data = ""; + else { + object.data = []; + if (options.bytes !== Array) + object.data = $util.newBuffer(object.data); + } + if (options.bytes === String) + object.nonSharedData = ""; + else { + object.nonSharedData = []; + if (options.bytes !== Array) + object.nonSharedData = $util.newBuffer(object.nonSharedData); + } + if (options.bytes === String) + object.folderUid = ""; + else { + object.folderUid = []; + if (options.bytes !== Array) + object.folderUid = $util.newBuffer(object.folderUid); + } + object.audit = null; + object.securityData = null; + object.securityScoreData = null; + } + if (message.recordUid != null && message.hasOwnProperty("recordUid")) + object.recordUid = options.bytes === String ? $util.base64.encode(message.recordUid, 0, message.recordUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.recordUid) : message.recordUid; + if (message.recordKey != null && message.hasOwnProperty("recordKey")) + object.recordKey = options.bytes === String ? $util.base64.encode(message.recordKey, 0, message.recordKey.length) : options.bytes === Array ? Array.prototype.slice.call(message.recordKey) : message.recordKey; + if (message.recordKeyType != null && message.hasOwnProperty("recordKeyType")) + object.recordKeyType = options.enums === String ? $root.Folder.EncryptedKeyType[message.recordKeyType] === undefined ? message.recordKeyType : $root.Folder.EncryptedKeyType[message.recordKeyType] : message.recordKeyType; + if (message.recordKeyEncryptedBy != null && message.hasOwnProperty("recordKeyEncryptedBy")) + object.recordKeyEncryptedBy = options.enums === String ? $root.Folder.FolderKeyEncryptionType[message.recordKeyEncryptedBy] === undefined ? message.recordKeyEncryptedBy : $root.Folder.FolderKeyEncryptionType[message.recordKeyEncryptedBy] : message.recordKeyEncryptedBy; + if (message.clientModifiedTime != null && message.hasOwnProperty("clientModifiedTime")) + if (typeof BigInt !== "undefined" && options.longs === BigInt) + object.clientModifiedTime = typeof message.clientModifiedTime === "number" ? BigInt(message.clientModifiedTime) : $util.Long.fromBits(message.clientModifiedTime.low >>> 0, message.clientModifiedTime.high >>> 0, false).toBigInt(); + else if (typeof message.clientModifiedTime === "number") + object.clientModifiedTime = options.longs === String ? String(message.clientModifiedTime) : message.clientModifiedTime; + else + object.clientModifiedTime = options.longs === String ? $util.Long.prototype.toString.call(message.clientModifiedTime) : options.longs === Number ? new $util.LongBits(message.clientModifiedTime.low >>> 0, message.clientModifiedTime.high >>> 0).toNumber() : message.clientModifiedTime; + if (message.data != null && message.hasOwnProperty("data")) + object.data = options.bytes === String ? $util.base64.encode(message.data, 0, message.data.length) : options.bytes === Array ? Array.prototype.slice.call(message.data) : message.data; + if (message.nonSharedData != null && message.hasOwnProperty("nonSharedData")) + object.nonSharedData = options.bytes === String ? $util.base64.encode(message.nonSharedData, 0, message.nonSharedData.length) : options.bytes === Array ? Array.prototype.slice.call(message.nonSharedData) : message.nonSharedData; + if (message.folderUid != null && message.hasOwnProperty("folderUid")) + object.folderUid = options.bytes === String ? $util.base64.encode(message.folderUid, 0, message.folderUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.folderUid) : message.folderUid; + if (message.recordLinks && message.recordLinks.length) { + object.recordLinks = []; + for (let j = 0; j < message.recordLinks.length; ++j) + object.recordLinks[j] = $root.Records.RecordLink.toObject(message.recordLinks[j], options, q + 1); + } + if (message.audit != null && message.hasOwnProperty("audit")) + object.audit = $root.Records.RecordAudit.toObject(message.audit, options, q + 1); + if (message.securityData != null && message.hasOwnProperty("securityData")) + object.securityData = $root.Records.SecurityData.toObject(message.securityData, options, q + 1); + if (message.securityScoreData != null && message.hasOwnProperty("securityScoreData")) + object.securityScoreData = $root.Records.SecurityScoreData.toObject(message.securityScoreData, options, q + 1); + return object; + }; + + /** + * Converts this RecordAdd to JSON. + * @function toJSON + * @memberof record.v3.RecordAdd + * @instance + * @returns {Object.} JSON object + */ + RecordAdd.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for RecordAdd + * @function getTypeUrl + * @memberof record.v3.RecordAdd + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + RecordAdd.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/record.v3.RecordAdd"; + }; + + return RecordAdd; + })(); + return v3; })(); @@ -301184,6 +302653,8 @@ export const Router = $root.Router = (() => { * @property {number|null} [clientVersionId] RouterRecordRotationRequest clientVersionId * @property {boolean|null} [noop] RouterRecordRotationRequest noop * @property {Uint8Array|null} [saasConfiguration] RouterRecordRotationRequest saasConfiguration + * @property {boolean|null} [updateServices] RouterRecordRotationRequest updateServices + * @property {PAM.IUidList|null} [serviceResources] RouterRecordRotationRequest serviceResources */ /** @@ -301297,6 +302768,22 @@ export const Router = $root.Router = (() => { */ RouterRecordRotationRequest.prototype.saasConfiguration = null; + /** + * RouterRecordRotationRequest updateServices. + * @member {boolean|null|undefined} updateServices + * @memberof Router.RouterRecordRotationRequest + * @instance + */ + RouterRecordRotationRequest.prototype.updateServices = null; + + /** + * RouterRecordRotationRequest serviceResources. + * @member {PAM.IUidList|null|undefined} serviceResources + * @memberof Router.RouterRecordRotationRequest + * @instance + */ + RouterRecordRotationRequest.prototype.serviceResources = null; + // OneOf field names bound to virtual getters and setters let $oneOfFields; @@ -301306,6 +302793,18 @@ export const Router = $root.Router = (() => { set: $util.oneOfSetter($oneOfFields) }); + // Virtual OneOf for proto3 optional field + Object.defineProperty(RouterRecordRotationRequest.prototype, "_updateServices", { + get: $util.oneOfGetter($oneOfFields = ["updateServices"]), + set: $util.oneOfSetter($oneOfFields) + }); + + // Virtual OneOf for proto3 optional field + Object.defineProperty(RouterRecordRotationRequest.prototype, "_serviceResources", { + get: $util.oneOfGetter($oneOfFields = ["serviceResources"]), + set: $util.oneOfSetter($oneOfFields) + }); + /** * Creates a new RouterRecordRotationRequest instance using the specified properties. * @function create @@ -301358,6 +302857,10 @@ export const Router = $root.Router = (() => { writer.uint32(/* id 11, wireType 0 =*/88).bool(message.noop); if (message.saasConfiguration != null && Object.hasOwnProperty.call(message, "saasConfiguration")) writer.uint32(/* id 12, wireType 2 =*/98).bytes(message.saasConfiguration); + if (message.updateServices != null && Object.hasOwnProperty.call(message, "updateServices")) + writer.uint32(/* id 13, wireType 0 =*/104).bool(message.updateServices); + if (message.serviceResources != null && Object.hasOwnProperty.call(message, "serviceResources")) + $root.PAM.UidList.encode(message.serviceResources, writer.uint32(/* id 14, wireType 2 =*/114).fork(), q + 1).ldelim(); return writer; }; @@ -301446,6 +302949,14 @@ export const Router = $root.Router = (() => { message.saasConfiguration = reader.bytes(); break; } + case 13: { + message.updateServices = reader.bool(); + break; + } + case 14: { + message.serviceResources = $root.PAM.UidList.decode(reader, reader.uint32(), undefined, long + 1); + break; + } default: reader.skipType(tag & 7, long); break; @@ -301524,6 +303035,19 @@ export const Router = $root.Router = (() => { if (!(message.saasConfiguration && typeof message.saasConfiguration.length === "number" || $util.isString(message.saasConfiguration))) return "saasConfiguration: buffer expected"; } + if (message.updateServices != null && message.hasOwnProperty("updateServices")) { + properties._updateServices = 1; + if (typeof message.updateServices !== "boolean") + return "updateServices: boolean expected"; + } + if (message.serviceResources != null && message.hasOwnProperty("serviceResources")) { + properties._serviceResources = 1; + { + let error = $root.PAM.UidList.verify(message.serviceResources, long + 1); + if (error) + return "serviceResources." + error; + } + } return null; }; @@ -301596,6 +303120,13 @@ export const Router = $root.Router = (() => { $util.base64.decode(object.saasConfiguration, message.saasConfiguration = $util.newBuffer($util.base64.length(object.saasConfiguration)), 0); else if (object.saasConfiguration.length >= 0) message.saasConfiguration = object.saasConfiguration; + if (object.updateServices != null) + message.updateServices = Boolean(object.updateServices); + if (object.serviceResources != null) { + if (typeof object.serviceResources !== "object") + throw TypeError(".Router.RouterRecordRotationRequest.serviceResources: object expected"); + message.serviceResources = $root.PAM.UidList.fromObject(object.serviceResources, long + 1); + } return message; }; @@ -301698,6 +303229,16 @@ export const Router = $root.Router = (() => { if (options.oneofs) object._saasConfiguration = "saasConfiguration"; } + if (message.updateServices != null && message.hasOwnProperty("updateServices")) { + object.updateServices = message.updateServices; + if (options.oneofs) + object._updateServices = "updateServices"; + } + if (message.serviceResources != null && message.hasOwnProperty("serviceResources")) { + object.serviceResources = $root.PAM.UidList.toObject(message.serviceResources, options, q + 1); + if (options.oneofs) + object._serviceResources = "serviceResources"; + } return object; }; @@ -308256,6 +309797,8 @@ export const Router = $root.Router = (() => { * @memberof Router * @interface IPAMNetworkSettings * @property {Uint8Array|null} [allowedSettings] PAMNetworkSettings allowedSettings + * @property {Uint8Array|null} [idpConfigUid] PAMNetworkSettings idpConfigUid + * @property {Uint8Array|null} [adminUid] PAMNetworkSettings adminUid */ /** @@ -308281,6 +309824,37 @@ export const Router = $root.Router = (() => { */ PAMNetworkSettings.prototype.allowedSettings = $util.newBuffer([]); + /** + * PAMNetworkSettings idpConfigUid. + * @member {Uint8Array|null|undefined} idpConfigUid + * @memberof Router.PAMNetworkSettings + * @instance + */ + PAMNetworkSettings.prototype.idpConfigUid = null; + + /** + * PAMNetworkSettings adminUid. + * @member {Uint8Array|null|undefined} adminUid + * @memberof Router.PAMNetworkSettings + * @instance + */ + PAMNetworkSettings.prototype.adminUid = null; + + // OneOf field names bound to virtual getters and setters + let $oneOfFields; + + // Virtual OneOf for proto3 optional field + Object.defineProperty(PAMNetworkSettings.prototype, "_idpConfigUid", { + get: $util.oneOfGetter($oneOfFields = ["idpConfigUid"]), + set: $util.oneOfSetter($oneOfFields) + }); + + // Virtual OneOf for proto3 optional field + Object.defineProperty(PAMNetworkSettings.prototype, "_adminUid", { + get: $util.oneOfGetter($oneOfFields = ["adminUid"]), + set: $util.oneOfSetter($oneOfFields) + }); + /** * Creates a new PAMNetworkSettings instance using the specified properties. * @function create @@ -308311,6 +309885,10 @@ export const Router = $root.Router = (() => { throw Error("max depth exceeded"); if (message.allowedSettings != null && Object.hasOwnProperty.call(message, "allowedSettings")) writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.allowedSettings); + if (message.idpConfigUid != null && Object.hasOwnProperty.call(message, "idpConfigUid")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.idpConfigUid); + if (message.adminUid != null && Object.hasOwnProperty.call(message, "adminUid")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.adminUid); return writer; }; @@ -308355,6 +309933,14 @@ export const Router = $root.Router = (() => { message.allowedSettings = reader.bytes(); break; } + case 2: { + message.idpConfigUid = reader.bytes(); + break; + } + case 3: { + message.adminUid = reader.bytes(); + break; + } default: reader.skipType(tag & 7, long); break; @@ -308394,9 +309980,20 @@ export const Router = $root.Router = (() => { long = 0; if (long > $util.recursionLimit) return "maximum nesting depth exceeded"; + let properties = {}; if (message.allowedSettings != null && message.hasOwnProperty("allowedSettings")) if (!(message.allowedSettings && typeof message.allowedSettings.length === "number" || $util.isString(message.allowedSettings))) return "allowedSettings: buffer expected"; + if (message.idpConfigUid != null && message.hasOwnProperty("idpConfigUid")) { + properties._idpConfigUid = 1; + if (!(message.idpConfigUid && typeof message.idpConfigUid.length === "number" || $util.isString(message.idpConfigUid))) + return "idpConfigUid: buffer expected"; + } + if (message.adminUid != null && message.hasOwnProperty("adminUid")) { + properties._adminUid = 1; + if (!(message.adminUid && typeof message.adminUid.length === "number" || $util.isString(message.adminUid))) + return "adminUid: buffer expected"; + } return null; }; @@ -308421,6 +310018,16 @@ export const Router = $root.Router = (() => { $util.base64.decode(object.allowedSettings, message.allowedSettings = $util.newBuffer($util.base64.length(object.allowedSettings)), 0); else if (object.allowedSettings.length >= 0) message.allowedSettings = object.allowedSettings; + if (object.idpConfigUid != null) + if (typeof object.idpConfigUid === "string") + $util.base64.decode(object.idpConfigUid, message.idpConfigUid = $util.newBuffer($util.base64.length(object.idpConfigUid)), 0); + else if (object.idpConfigUid.length >= 0) + message.idpConfigUid = object.idpConfigUid; + if (object.adminUid != null) + if (typeof object.adminUid === "string") + $util.base64.decode(object.adminUid, message.adminUid = $util.newBuffer($util.base64.length(object.adminUid)), 0); + else if (object.adminUid.length >= 0) + message.adminUid = object.adminUid; return message; }; @@ -308451,6 +310058,16 @@ export const Router = $root.Router = (() => { } if (message.allowedSettings != null && message.hasOwnProperty("allowedSettings")) object.allowedSettings = options.bytes === String ? $util.base64.encode(message.allowedSettings, 0, message.allowedSettings.length) : options.bytes === Array ? Array.prototype.slice.call(message.allowedSettings) : message.allowedSettings; + if (message.idpConfigUid != null && message.hasOwnProperty("idpConfigUid")) { + object.idpConfigUid = options.bytes === String ? $util.base64.encode(message.idpConfigUid, 0, message.idpConfigUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.idpConfigUid) : message.idpConfigUid; + if (options.oneofs) + object._idpConfigUid = "idpConfigUid"; + } + if (message.adminUid != null && message.hasOwnProperty("adminUid")) { + object.adminUid = options.bytes === String ? $util.base64.encode(message.adminUid, 0, message.adminUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.adminUid) : message.adminUid; + if (options.oneofs) + object._adminUid = "adminUid"; + } return object; }; @@ -320768,6 +322385,7 @@ export const PAM = $root.PAM = (() => { * @property {Uint8Array|null} [domainUid] PAMResourceConfig domainUid * @property {Uint8Array|null} [jitSettings] PAMResourceConfig jitSettings * @property {Uint8Array|null} [keeperAiSettings] PAMResourceConfig keeperAiSettings + * @property {boolean|null} [updateServices] PAMResourceConfig updateServices */ /** @@ -320857,6 +322475,14 @@ export const PAM = $root.PAM = (() => { */ PAMResourceConfig.prototype.keeperAiSettings = null; + /** + * PAMResourceConfig updateServices. + * @member {boolean|null|undefined} updateServices + * @memberof PAM.PAMResourceConfig + * @instance + */ + PAMResourceConfig.prototype.updateServices = null; + // OneOf field names bound to virtual getters and setters let $oneOfFields; @@ -320908,6 +322534,12 @@ export const PAM = $root.PAM = (() => { set: $util.oneOfSetter($oneOfFields) }); + // Virtual OneOf for proto3 optional field + Object.defineProperty(PAMResourceConfig.prototype, "_updateServices", { + get: $util.oneOfGetter($oneOfFields = ["updateServices"]), + set: $util.oneOfSetter($oneOfFields) + }); + /** * Creates a new PAMResourceConfig instance using the specified properties. * @function create @@ -320954,6 +322586,8 @@ export const PAM = $root.PAM = (() => { writer.uint32(/* id 8, wireType 2 =*/66).bytes(message.jitSettings); if (message.keeperAiSettings != null && Object.hasOwnProperty.call(message, "keeperAiSettings")) writer.uint32(/* id 9, wireType 2 =*/74).bytes(message.keeperAiSettings); + if (message.updateServices != null && Object.hasOwnProperty.call(message, "updateServices")) + writer.uint32(/* id 10, wireType 0 =*/80).bool(message.updateServices); return writer; }; @@ -321030,6 +322664,10 @@ export const PAM = $root.PAM = (() => { message.keeperAiSettings = reader.bytes(); break; } + case 10: { + message.updateServices = reader.bool(); + break; + } default: reader.skipType(tag & 7, long); break; @@ -321116,6 +322754,11 @@ export const PAM = $root.PAM = (() => { if (!(message.keeperAiSettings && typeof message.keeperAiSettings.length === "number" || $util.isString(message.keeperAiSettings))) return "keeperAiSettings: buffer expected"; } + if (message.updateServices != null && message.hasOwnProperty("updateServices")) { + properties._updateServices = 1; + if (typeof message.updateServices !== "boolean") + return "updateServices: boolean expected"; + } return null; }; @@ -321180,6 +322823,8 @@ export const PAM = $root.PAM = (() => { $util.base64.decode(object.keeperAiSettings, message.keeperAiSettings = $util.newBuffer($util.base64.length(object.keeperAiSettings)), 0); else if (object.keeperAiSettings.length >= 0) message.keeperAiSettings = object.keeperAiSettings; + if (object.updateServices != null) + message.updateServices = Boolean(object.updateServices); return message; }; @@ -321250,6 +322895,11 @@ export const PAM = $root.PAM = (() => { if (options.oneofs) object._keeperAiSettings = "keeperAiSettings"; } + if (message.updateServices != null && message.hasOwnProperty("updateServices")) { + object.updateServices = message.updateServices; + if (options.oneofs) + object._updateServices = "updateServices"; + } return object; }; @@ -324547,6 +326197,787 @@ export const PAM = $root.PAM = (() => { return GetNhiUidsResponse; })(); + PAM.PAMUniversalSyncPreCheckRequest = (function() { + + /** + * Properties of a PAMUniversalSyncPreCheckRequest. + * @memberof PAM + * @interface IPAMUniversalSyncPreCheckRequest + * @property {Uint8Array|null} [networkUid] PAMUniversalSyncPreCheckRequest networkUid + * @property {Array.|null} [folderUids] PAMUniversalSyncPreCheckRequest folderUids + */ + + /** + * Constructs a new PAMUniversalSyncPreCheckRequest. + * @memberof PAM + * @classdesc Represents a PAMUniversalSyncPreCheckRequest. + * @implements IPAMUniversalSyncPreCheckRequest + * @constructor + * @param {PAM.IPAMUniversalSyncPreCheckRequest=} [properties] Properties to set + */ + function PAMUniversalSyncPreCheckRequest(properties) { + this.folderUids = []; + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + } + + /** + * PAMUniversalSyncPreCheckRequest networkUid. + * @member {Uint8Array} networkUid + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @instance + */ + PAMUniversalSyncPreCheckRequest.prototype.networkUid = $util.newBuffer([]); + + /** + * PAMUniversalSyncPreCheckRequest folderUids. + * @member {Array.} folderUids + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @instance + */ + PAMUniversalSyncPreCheckRequest.prototype.folderUids = $util.emptyArray; + + /** + * Creates a new PAMUniversalSyncPreCheckRequest instance using the specified properties. + * @function create + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {PAM.IPAMUniversalSyncPreCheckRequest=} [properties] Properties to set + * @returns {PAM.PAMUniversalSyncPreCheckRequest} PAMUniversalSyncPreCheckRequest instance + */ + PAMUniversalSyncPreCheckRequest.create = function create(properties) { + return new PAMUniversalSyncPreCheckRequest(properties); + }; + + /** + * Encodes the specified PAMUniversalSyncPreCheckRequest message. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckRequest.verify|verify} messages. + * @function encode + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {PAM.IPAMUniversalSyncPreCheckRequest} message PAMUniversalSyncPreCheckRequest message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PAMUniversalSyncPreCheckRequest.encode = function encode(message, writer, q) { + if (!writer) + writer = $Writer.create(); + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + if (message.networkUid != null && Object.hasOwnProperty.call(message, "networkUid")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.networkUid); + if (message.folderUids != null && message.folderUids.length) + for (let i = 0; i < message.folderUids.length; ++i) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.folderUids[i]); + return writer; + }; + + /** + * Encodes the specified PAMUniversalSyncPreCheckRequest message, length delimited. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckRequest.verify|verify} messages. + * @function encodeDelimited + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {PAM.IPAMUniversalSyncPreCheckRequest} message PAMUniversalSyncPreCheckRequest message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PAMUniversalSyncPreCheckRequest.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a PAMUniversalSyncPreCheckRequest message from the specified reader or buffer. + * @function decode + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {PAM.PAMUniversalSyncPreCheckRequest} PAMUniversalSyncPreCheckRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PAMUniversalSyncPreCheckRequest.decode = function decode(reader, length, error, long) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (long === undefined) + long = 0; + if (long > $Reader.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.PAM.PAMUniversalSyncPreCheckRequest(); + while (reader.pos < end) { + let tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.networkUid = reader.bytes(); + break; + } + case 2: { + if (!(message.folderUids && message.folderUids.length)) + message.folderUids = []; + message.folderUids.push(reader.bytes()); + break; + } + default: + reader.skipType(tag & 7, long); + break; + } + } + return message; + }; + + /** + * Decodes a PAMUniversalSyncPreCheckRequest message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {PAM.PAMUniversalSyncPreCheckRequest} PAMUniversalSyncPreCheckRequest + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PAMUniversalSyncPreCheckRequest.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a PAMUniversalSyncPreCheckRequest message. + * @function verify + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + PAMUniversalSyncPreCheckRequest.verify = function verify(message, long) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + return "maximum nesting depth exceeded"; + if (message.networkUid != null && message.hasOwnProperty("networkUid")) + if (!(message.networkUid && typeof message.networkUid.length === "number" || $util.isString(message.networkUid))) + return "networkUid: buffer expected"; + if (message.folderUids != null && message.hasOwnProperty("folderUids")) { + if (!Array.isArray(message.folderUids)) + return "folderUids: array expected"; + for (let i = 0; i < message.folderUids.length; ++i) + if (!(message.folderUids[i] && typeof message.folderUids[i].length === "number" || $util.isString(message.folderUids[i]))) + return "folderUids: buffer[] expected"; + } + return null; + }; + + /** + * Creates a PAMUniversalSyncPreCheckRequest message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {Object.} object Plain object + * @returns {PAM.PAMUniversalSyncPreCheckRequest} PAMUniversalSyncPreCheckRequest + */ + PAMUniversalSyncPreCheckRequest.fromObject = function fromObject(object, long) { + if (object instanceof $root.PAM.PAMUniversalSyncPreCheckRequest) + return object; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let message = new $root.PAM.PAMUniversalSyncPreCheckRequest(); + if (object.networkUid != null) + if (typeof object.networkUid === "string") + $util.base64.decode(object.networkUid, message.networkUid = $util.newBuffer($util.base64.length(object.networkUid)), 0); + else if (object.networkUid.length >= 0) + message.networkUid = object.networkUid; + if (object.folderUids) { + if (!Array.isArray(object.folderUids)) + throw TypeError(".PAM.PAMUniversalSyncPreCheckRequest.folderUids: array expected"); + message.folderUids = []; + for (let i = 0; i < object.folderUids.length; ++i) + if (typeof object.folderUids[i] === "string") + $util.base64.decode(object.folderUids[i], message.folderUids[i] = $util.newBuffer($util.base64.length(object.folderUids[i])), 0); + else if (object.folderUids[i].length >= 0) + message.folderUids[i] = object.folderUids[i]; + } + return message; + }; + + /** + * Creates a plain object from a PAMUniversalSyncPreCheckRequest message. Also converts values to other types if specified. + * @function toObject + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {PAM.PAMUniversalSyncPreCheckRequest} message PAMUniversalSyncPreCheckRequest + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + PAMUniversalSyncPreCheckRequest.toObject = function toObject(message, options, q) { + if (!options) + options = {}; + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.folderUids = []; + if (options.defaults) + if (options.bytes === String) + object.networkUid = ""; + else { + object.networkUid = []; + if (options.bytes !== Array) + object.networkUid = $util.newBuffer(object.networkUid); + } + if (message.networkUid != null && message.hasOwnProperty("networkUid")) + object.networkUid = options.bytes === String ? $util.base64.encode(message.networkUid, 0, message.networkUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.networkUid) : message.networkUid; + if (message.folderUids && message.folderUids.length) { + object.folderUids = []; + for (let j = 0; j < message.folderUids.length; ++j) + object.folderUids[j] = options.bytes === String ? $util.base64.encode(message.folderUids[j], 0, message.folderUids[j].length) : options.bytes === Array ? Array.prototype.slice.call(message.folderUids[j]) : message.folderUids[j]; + } + return object; + }; + + /** + * Converts this PAMUniversalSyncPreCheckRequest to JSON. + * @function toJSON + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @instance + * @returns {Object.} JSON object + */ + PAMUniversalSyncPreCheckRequest.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for PAMUniversalSyncPreCheckRequest + * @function getTypeUrl + * @memberof PAM.PAMUniversalSyncPreCheckRequest + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + PAMUniversalSyncPreCheckRequest.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/PAM.PAMUniversalSyncPreCheckRequest"; + }; + + return PAMUniversalSyncPreCheckRequest; + })(); + + PAM.PAMUniversalSyncPreCheckResult = (function() { + + /** + * Properties of a PAMUniversalSyncPreCheckResult. + * @memberof PAM + * @interface IPAMUniversalSyncPreCheckResult + * @property {Uint8Array|null} [folderUid] PAMUniversalSyncPreCheckResult folderUid + * @property {boolean|null} [isUsed] PAMUniversalSyncPreCheckResult isUsed + */ + + /** + * Constructs a new PAMUniversalSyncPreCheckResult. + * @memberof PAM + * @classdesc Represents a PAMUniversalSyncPreCheckResult. + * @implements IPAMUniversalSyncPreCheckResult + * @constructor + * @param {PAM.IPAMUniversalSyncPreCheckResult=} [properties] Properties to set + */ + function PAMUniversalSyncPreCheckResult(properties) { + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + } + + /** + * PAMUniversalSyncPreCheckResult folderUid. + * @member {Uint8Array} folderUid + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @instance + */ + PAMUniversalSyncPreCheckResult.prototype.folderUid = $util.newBuffer([]); + + /** + * PAMUniversalSyncPreCheckResult isUsed. + * @member {boolean} isUsed + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @instance + */ + PAMUniversalSyncPreCheckResult.prototype.isUsed = false; + + /** + * Creates a new PAMUniversalSyncPreCheckResult instance using the specified properties. + * @function create + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {PAM.IPAMUniversalSyncPreCheckResult=} [properties] Properties to set + * @returns {PAM.PAMUniversalSyncPreCheckResult} PAMUniversalSyncPreCheckResult instance + */ + PAMUniversalSyncPreCheckResult.create = function create(properties) { + return new PAMUniversalSyncPreCheckResult(properties); + }; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResult message. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResult.verify|verify} messages. + * @function encode + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {PAM.IPAMUniversalSyncPreCheckResult} message PAMUniversalSyncPreCheckResult message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PAMUniversalSyncPreCheckResult.encode = function encode(message, writer, q) { + if (!writer) + writer = $Writer.create(); + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + if (message.folderUid != null && Object.hasOwnProperty.call(message, "folderUid")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.folderUid); + if (message.isUsed != null && Object.hasOwnProperty.call(message, "isUsed")) + writer.uint32(/* id 2, wireType 0 =*/16).bool(message.isUsed); + return writer; + }; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResult message, length delimited. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResult.verify|verify} messages. + * @function encodeDelimited + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {PAM.IPAMUniversalSyncPreCheckResult} message PAMUniversalSyncPreCheckResult message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PAMUniversalSyncPreCheckResult.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a PAMUniversalSyncPreCheckResult message from the specified reader or buffer. + * @function decode + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {PAM.PAMUniversalSyncPreCheckResult} PAMUniversalSyncPreCheckResult + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PAMUniversalSyncPreCheckResult.decode = function decode(reader, length, error, long) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (long === undefined) + long = 0; + if (long > $Reader.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.PAM.PAMUniversalSyncPreCheckResult(); + while (reader.pos < end) { + let tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.folderUid = reader.bytes(); + break; + } + case 2: { + message.isUsed = reader.bool(); + break; + } + default: + reader.skipType(tag & 7, long); + break; + } + } + return message; + }; + + /** + * Decodes a PAMUniversalSyncPreCheckResult message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {PAM.PAMUniversalSyncPreCheckResult} PAMUniversalSyncPreCheckResult + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PAMUniversalSyncPreCheckResult.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a PAMUniversalSyncPreCheckResult message. + * @function verify + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + PAMUniversalSyncPreCheckResult.verify = function verify(message, long) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + return "maximum nesting depth exceeded"; + if (message.folderUid != null && message.hasOwnProperty("folderUid")) + if (!(message.folderUid && typeof message.folderUid.length === "number" || $util.isString(message.folderUid))) + return "folderUid: buffer expected"; + if (message.isUsed != null && message.hasOwnProperty("isUsed")) + if (typeof message.isUsed !== "boolean") + return "isUsed: boolean expected"; + return null; + }; + + /** + * Creates a PAMUniversalSyncPreCheckResult message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {Object.} object Plain object + * @returns {PAM.PAMUniversalSyncPreCheckResult} PAMUniversalSyncPreCheckResult + */ + PAMUniversalSyncPreCheckResult.fromObject = function fromObject(object, long) { + if (object instanceof $root.PAM.PAMUniversalSyncPreCheckResult) + return object; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let message = new $root.PAM.PAMUniversalSyncPreCheckResult(); + if (object.folderUid != null) + if (typeof object.folderUid === "string") + $util.base64.decode(object.folderUid, message.folderUid = $util.newBuffer($util.base64.length(object.folderUid)), 0); + else if (object.folderUid.length >= 0) + message.folderUid = object.folderUid; + if (object.isUsed != null) + message.isUsed = Boolean(object.isUsed); + return message; + }; + + /** + * Creates a plain object from a PAMUniversalSyncPreCheckResult message. Also converts values to other types if specified. + * @function toObject + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {PAM.PAMUniversalSyncPreCheckResult} message PAMUniversalSyncPreCheckResult + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + PAMUniversalSyncPreCheckResult.toObject = function toObject(message, options, q) { + if (!options) + options = {}; + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + if (options.bytes === String) + object.folderUid = ""; + else { + object.folderUid = []; + if (options.bytes !== Array) + object.folderUid = $util.newBuffer(object.folderUid); + } + object.isUsed = false; + } + if (message.folderUid != null && message.hasOwnProperty("folderUid")) + object.folderUid = options.bytes === String ? $util.base64.encode(message.folderUid, 0, message.folderUid.length) : options.bytes === Array ? Array.prototype.slice.call(message.folderUid) : message.folderUid; + if (message.isUsed != null && message.hasOwnProperty("isUsed")) + object.isUsed = message.isUsed; + return object; + }; + + /** + * Converts this PAMUniversalSyncPreCheckResult to JSON. + * @function toJSON + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @instance + * @returns {Object.} JSON object + */ + PAMUniversalSyncPreCheckResult.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for PAMUniversalSyncPreCheckResult + * @function getTypeUrl + * @memberof PAM.PAMUniversalSyncPreCheckResult + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + PAMUniversalSyncPreCheckResult.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/PAM.PAMUniversalSyncPreCheckResult"; + }; + + return PAMUniversalSyncPreCheckResult; + })(); + + PAM.PAMUniversalSyncPreCheckResponse = (function() { + + /** + * Properties of a PAMUniversalSyncPreCheckResponse. + * @memberof PAM + * @interface IPAMUniversalSyncPreCheckResponse + * @property {Array.|null} [results] PAMUniversalSyncPreCheckResponse results + */ + + /** + * Constructs a new PAMUniversalSyncPreCheckResponse. + * @memberof PAM + * @classdesc Represents a PAMUniversalSyncPreCheckResponse. + * @implements IPAMUniversalSyncPreCheckResponse + * @constructor + * @param {PAM.IPAMUniversalSyncPreCheckResponse=} [properties] Properties to set + */ + function PAMUniversalSyncPreCheckResponse(properties) { + this.results = []; + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + } + + /** + * PAMUniversalSyncPreCheckResponse results. + * @member {Array.} results + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @instance + */ + PAMUniversalSyncPreCheckResponse.prototype.results = $util.emptyArray; + + /** + * Creates a new PAMUniversalSyncPreCheckResponse instance using the specified properties. + * @function create + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {PAM.IPAMUniversalSyncPreCheckResponse=} [properties] Properties to set + * @returns {PAM.PAMUniversalSyncPreCheckResponse} PAMUniversalSyncPreCheckResponse instance + */ + PAMUniversalSyncPreCheckResponse.create = function create(properties) { + return new PAMUniversalSyncPreCheckResponse(properties); + }; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResponse message. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResponse.verify|verify} messages. + * @function encode + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {PAM.IPAMUniversalSyncPreCheckResponse} message PAMUniversalSyncPreCheckResponse message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PAMUniversalSyncPreCheckResponse.encode = function encode(message, writer, q) { + if (!writer) + writer = $Writer.create(); + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + if (message.results != null && message.results.length) + for (let i = 0; i < message.results.length; ++i) + $root.PAM.PAMUniversalSyncPreCheckResult.encode(message.results[i], writer.uint32(/* id 1, wireType 2 =*/10).fork(), q + 1).ldelim(); + return writer; + }; + + /** + * Encodes the specified PAMUniversalSyncPreCheckResponse message, length delimited. Does not implicitly {@link PAM.PAMUniversalSyncPreCheckResponse.verify|verify} messages. + * @function encodeDelimited + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {PAM.IPAMUniversalSyncPreCheckResponse} message PAMUniversalSyncPreCheckResponse message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PAMUniversalSyncPreCheckResponse.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a PAMUniversalSyncPreCheckResponse message from the specified reader or buffer. + * @function decode + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {PAM.PAMUniversalSyncPreCheckResponse} PAMUniversalSyncPreCheckResponse + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PAMUniversalSyncPreCheckResponse.decode = function decode(reader, length, error, long) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (long === undefined) + long = 0; + if (long > $Reader.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.PAM.PAMUniversalSyncPreCheckResponse(); + while (reader.pos < end) { + let tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + if (!(message.results && message.results.length)) + message.results = []; + message.results.push($root.PAM.PAMUniversalSyncPreCheckResult.decode(reader, reader.uint32(), undefined, long + 1)); + break; + } + default: + reader.skipType(tag & 7, long); + break; + } + } + return message; + }; + + /** + * Decodes a PAMUniversalSyncPreCheckResponse message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {PAM.PAMUniversalSyncPreCheckResponse} PAMUniversalSyncPreCheckResponse + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PAMUniversalSyncPreCheckResponse.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a PAMUniversalSyncPreCheckResponse message. + * @function verify + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + PAMUniversalSyncPreCheckResponse.verify = function verify(message, long) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + return "maximum nesting depth exceeded"; + if (message.results != null && message.hasOwnProperty("results")) { + if (!Array.isArray(message.results)) + return "results: array expected"; + for (let i = 0; i < message.results.length; ++i) { + let error = $root.PAM.PAMUniversalSyncPreCheckResult.verify(message.results[i], long + 1); + if (error) + return "results." + error; + } + } + return null; + }; + + /** + * Creates a PAMUniversalSyncPreCheckResponse message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {Object.} object Plain object + * @returns {PAM.PAMUniversalSyncPreCheckResponse} PAMUniversalSyncPreCheckResponse + */ + PAMUniversalSyncPreCheckResponse.fromObject = function fromObject(object, long) { + if (object instanceof $root.PAM.PAMUniversalSyncPreCheckResponse) + return object; + if (long === undefined) + long = 0; + if (long > $util.recursionLimit) + throw Error("maximum nesting depth exceeded"); + let message = new $root.PAM.PAMUniversalSyncPreCheckResponse(); + if (object.results) { + if (!Array.isArray(object.results)) + throw TypeError(".PAM.PAMUniversalSyncPreCheckResponse.results: array expected"); + message.results = []; + for (let i = 0; i < object.results.length; ++i) { + if (typeof object.results[i] !== "object") + throw TypeError(".PAM.PAMUniversalSyncPreCheckResponse.results: object expected"); + message.results[i] = $root.PAM.PAMUniversalSyncPreCheckResult.fromObject(object.results[i], long + 1); + } + } + return message; + }; + + /** + * Creates a plain object from a PAMUniversalSyncPreCheckResponse message. Also converts values to other types if specified. + * @function toObject + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {PAM.PAMUniversalSyncPreCheckResponse} message PAMUniversalSyncPreCheckResponse + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + PAMUniversalSyncPreCheckResponse.toObject = function toObject(message, options, q) { + if (!options) + options = {}; + if (q === undefined) + q = 0; + if (q > $util.recursionLimit) + throw Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.results = []; + if (message.results && message.results.length) { + object.results = []; + for (let j = 0; j < message.results.length; ++j) + object.results[j] = $root.PAM.PAMUniversalSyncPreCheckResult.toObject(message.results[j], options, q + 1); + } + return object; + }; + + /** + * Converts this PAMUniversalSyncPreCheckResponse to JSON. + * @function toJSON + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @instance + * @returns {Object.} JSON object + */ + PAMUniversalSyncPreCheckResponse.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for PAMUniversalSyncPreCheckResponse + * @function getTypeUrl + * @memberof PAM.PAMUniversalSyncPreCheckResponse + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + PAMUniversalSyncPreCheckResponse.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/PAM.PAMUniversalSyncPreCheckResponse"; + }; + + return PAMUniversalSyncPreCheckResponse; + })(); + return PAM; })(); diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index bd5e257..2a36bef 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -17,6 +17,7 @@ import { Vault, Tokens, NotificationCenter, + record, } from './proto' // generated protobuf has all properties optional and nullable, while this is not an issue for KeeperApp, this type fixes it @@ -957,3 +958,13 @@ export const pamGetLeafsMessage = ( export const pamGetOnlineControllersMessage = (): RestOutMessage => createOutMessage('api/user/get_controllers', PAM.PAMOnlineControllers) + +export const keeperDriveRecordsAdd = ( + data: record.v3.IRecordsAddRequest +): RestMessage => + createMessage(data, '/vault/records/v3/add', record.v3.RecordsAddRequest, Records.RecordsModifyResponse) + +export const keeperDriveRecordsUpdate = ( + data: Records.IRecordsUpdateRequest +): RestMessage => + createMessage(data, '/vault/records/v3/update', Records.RecordsUpdateRequest, Records.RecordsModifyResponse) diff --git a/keeperapi/src/syncDown/index.ts b/keeperapi/src/syncDown/index.ts new file mode 100644 index 0000000..ce4acb5 --- /dev/null +++ b/keeperapi/src/syncDown/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './utils' diff --git a/keeperapi/src/syncDown/types.ts b/keeperapi/src/syncDown/types.ts new file mode 100644 index 0000000..00fc7ca --- /dev/null +++ b/keeperapi/src/syncDown/types.ts @@ -0,0 +1,80 @@ +import { Folder, common } from '../proto' + +export type DKdFolder = { + kind: 'keeper_drive_folder' + uid: string + data: { + name: string + } + parentUid?: string + ownerInfo: { + accountUid?: string + username?: string + } + lastModified?: number + type?: Folder.FolderUsageType + inheritUserPermissions?: Folder.SetBooleanValue +} + +export type DKdFolderAccess = { + kind: 'keeper_drive_folder_access' + accessUid: string + folderUid: string + accessTypeUid: string + accessType: Folder.AccessType + accessRoleType: Folder.AccessRoleType + permission: Folder.IFolderPermissions + inherited?: boolean + hidden?: boolean +} + +export type DKdFolderSharingState = { + kind: 'keeper_drive_folder_sharing_state' + folderUid: string + count?: number + shared?: boolean +} + +export type DKdFolderRecord = { + kind: 'keeper_drive_folder_record' + folderUid: string + recordUid: string +} + +export type DKdRecordAccess = { + kind: 'keeper_drive_record_access' + accessUid: string + accessTypeUid: string + accessType: Folder.AccessType + recordUid: string + accessRoleType: Folder.AccessRoleType + owner?: boolean + inherited?: boolean + hidden?: boolean + deniedAccess?: boolean + canEdit?: boolean + canView?: boolean + canListAccess?: boolean + canUpdateAccess?: boolean + canDelete?: boolean + canChangeOwnership?: boolean + canRequestAccess?: boolean + canApproveAccess?: boolean + dateCreated?: number + lastModified?: number + tlaProperties?: common.tla.ITLAProperties +} + +export type DKdRecordSharingState = { + kind: 'keeper_drive_record_sharing_state' + recordUid: string + isDirectlyShared?: boolean + isIndirectlyShared?: boolean + isShared?: boolean +} + +export type DKdRecordLink = { + kind: 'keeper_drive_record_link' + parentRecordUid: string + childRecordUid: string +} diff --git a/keeperapi/src/syncDown/utils.ts b/keeperapi/src/syncDown/utils.ts new file mode 100644 index 0000000..62cc8d9 --- /dev/null +++ b/keeperapi/src/syncDown/utils.ts @@ -0,0 +1,30 @@ +import { Folder } from '../proto' +import { EncryptionType } from '../platform' + +export const createKdRecordAccessCompositeKey = (actorUid: string, recordUid: string) => `${actorUid}:${recordUid}` + +export const createKdFolderAccessCompositeKey = (actorUid: string, folderUid: string) => `${folderUid}:${actorUid}` + +export const mapTeamKeyType = (keyType: Folder.EncryptedKeyType, teamUid: string) => { + let keyId: string + let encryptionType: EncryptionType + const unwrappedType = 'aes' + switch (keyType) { + case Folder.EncryptedKeyType.encrypted_by_data_key: + keyId = teamUid + encryptionType = 'cbc' + break + case Folder.EncryptedKeyType.encrypted_by_public_key: + keyId = `${teamUid}_priv` + encryptionType = 'rsa' + break + case Folder.EncryptedKeyType.encrypted_by_public_key_ecc: + keyId = `${teamUid}_ecc` + encryptionType = 'ecc' + break + default: + console.error('Unknown key type: ' + keyType) + return null + } + return { keyId, encryptionType, unwrappedType } +} diff --git a/keeperapi/src/utils.ts b/keeperapi/src/utils.ts index 19719f2..e0b4e4a 100644 --- a/keeperapi/src/utils.ts +++ b/keeperapi/src/utils.ts @@ -243,3 +243,11 @@ export function resolvablePromise(): { promise: Promise; resolve: () => vo resolve: resolver, } } + +export const isNil = (value: T | null | undefined): value is null | undefined => { + return value == null +} + +export const toOptional = (value: T | null | undefined): T | undefined => { + return value ?? undefined +} diff --git a/keeperapi/src/vault.ts b/keeperapi/src/vault.ts index f744ff4..7ea8b9b 100644 --- a/keeperapi/src/vault.ts +++ b/keeperapi/src/vault.ts @@ -1,8 +1,8 @@ import { Auth } from './auth' import { NN, syncDownMessage } from './restMessages' import { CryptoWorkerOptions, EncryptionType, KeyStorage, platform } from './platform' -import { Records, Tokens, Vault } from './proto' -import { formatTimeDiff, webSafe64FromBytes } from './utils' +import { Folder, record, Records, Tokens, Vault } from './proto' +import { formatTimeDiff, isNil, toOptional, webSafe64FromBytes } from './utils' import { logger } from './log' import CacheStatus = Vault.CacheStatus import RecordKeyType = Records.RecordKeyType @@ -26,6 +26,22 @@ import IProfile = Vault.IProfile import IBreachWatchRecord = Vault.IBreachWatchRecord import IBreachWatchSecurityData = Vault.IBreachWatchSecurityData import type { UnwrapKeyMap } from './platform' +import { + createKdRecordAccessCompositeKey, + DKdFolderRecord, + DKdRecordAccess, + DKdRecordSharingState, + DKdFolder, + DKdFolderAccess, + DKdFolderSharingState, + mapTeamKeyType, + DKdRecordLink, + createKdFolderAccessCompositeKey, +} from './syncDown' + +export type VaultStorageDeleteOption = + | { kind: 'keeper_drive_record_access'; actorUid: string; recordUid: string } + | { kind: 'keeper_drive_folder_access'; folderUid: string; actorUid: string } export type VaultStorage = KeyStorage & { put(data: VaultStorageData): Promise @@ -34,7 +50,7 @@ export type VaultStorage = KeyStorage & { removeDependencies(dependencies: RemovedDependencies): Promise clear(): Promise get(kind: T, uid?: string): Promise> - delete(kind: VaultStorageKind, uid: string | Uint8Array): Promise + delete(kind: VaultStorageKind, uid: string | Uint8Array, option?: VaultStorageDeleteOption): Promise } export type VaultStorageData = @@ -57,27 +73,15 @@ export type VaultStorageData = | DSecurityScoreData | DUser | DRecordRotation + | DKdFolderRecord + | DKdRecordAccess + | DKdRecordSharingState + | DKdFolder + | DKdFolderSharingState + | DKdFolderAccess + | DKdRecordLink -export type VaultStorageKind = - | 'profilePic' - | 'record' - | 'metadata' - | 'non_shared_data' - | 'team' - | 'shared_folder' - | 'shared_folder_user' - | 'shared_folder_team' - | 'shared_folder_record' - | 'shared_folder_folder' - | 'user_folder' - | 'profile' - | 'continuationToken' - | 'reused_passwords' - | 'bw_record' - | 'bw_security_data' - | 'security_score_data' - | 'user' - | 'record_rotation' +export type VaultStorageKind = VaultStorageData['kind'] export type VaultStorageResult = | (T extends 'continuationToken' ? DContinuationToken : T extends 'record' ? DRecord : never) @@ -87,7 +91,9 @@ type MappedCounts = { [Property in keyof Type]: number } -type SyncResponseCounts = MappedCounts +type SyncResponseCounts = Partial< + MappedCounts & Vault.IKeeperDriveData> +> export type SyncResult = { started: Date @@ -115,6 +121,7 @@ export type DRecord = { clientModifiedTime: number extra?: any udata?: Udata + isKeeperDriveData?: boolean } export type DRecordMetadata = { @@ -285,9 +292,14 @@ export type Dependency = { parentUid: string uid: string } +export type RemovedDependency = { + parentKind: VaultStorageKind + childKind: VaultStorageKind + childUid: string +} export type DependencyMap = Record export type Dependencies = Record> -export type RemovedDependencies = Record | '*'> +export type RemovedDependencies = Record | '*'> const addDependencies = (dependencies: Dependencies, parentUid: string, childUid: string, kind: VaultStorageKind) => { let children = dependencies[parentUid] @@ -302,13 +314,17 @@ const addDependencies = (dependencies: Dependencies, parentUid: string, childUid }) } -const addRemovedDependencies = (dependencies: RemovedDependencies, parentUid: string, childUid: string) => { +const addRemovedDependencies = ( + dependencies: RemovedDependencies, + parentUid: string, + childUid: string | RemovedDependency +) => { let children = dependencies[parentUid] if (children === '*') { return } if (!children) { - children = new Set() + children = new Set() dependencies[parentUid] = children } children.add(childUid) @@ -331,26 +347,33 @@ const getDependencies = async (folderUid: string, storage: VaultStorage, results } } -const mapKeyType = (keyType: Records.RecordKeyType): { keyId: string; encryptionType: EncryptionType } | null => { +const mapKeyType = ( + keyType: Records.RecordKeyType | Folder.EncryptedKeyType +): { keyId: string; encryptionType: EncryptionType } | null => { let keyId: string let encryptionType: EncryptionType switch (keyType) { case RecordKeyType.NO_KEY: + case Folder.EncryptedKeyType.no_key: return null case RecordKeyType.ENCRYPTED_BY_DATA_KEY: + case Folder.EncryptedKeyType.encrypted_by_data_key: keyId = 'data' encryptionType = 'cbc' break case RecordKeyType.ENCRYPTED_BY_DATA_KEY_GCM: + case Folder.EncryptedKeyType.encrypted_by_data_key_gcm: keyId = 'data' encryptionType = 'gcm' break // RSA TAGGED - might have to fallback to ecc or force ecc - dont make a change here, rely on keeperapp to provide the correct keyType case RecordKeyType.ENCRYPTED_BY_PUBLIC_KEY: + case Folder.EncryptedKeyType.encrypted_by_public_key: keyId = 'pk_rsa' encryptionType = 'rsa' break case RecordKeyType.ENCRYPTED_BY_PUBLIC_KEY_ECC: + case Folder.EncryptedKeyType.encrypted_by_public_key_ecc: keyId = 'pk_ecc' encryptionType = 'ecc' break @@ -751,7 +774,8 @@ const processRecords = async (records: IRecord[], storage: VaultStorage) => { } } -const processNonSharedData = async (nonSharedData: INonSharedData[], storage: VaultStorage) => { +const processNonSharedData = async (storage: VaultStorage, nonSharedData?: Vault.INonSharedData[] | null) => { + if (!nonSharedData) return for (const nsData of nonSharedData as NN[]) { const recUid = webSafe64FromBytes(nsData.recordUid) try { @@ -988,7 +1012,8 @@ const processMetadata = async (recordMetaData: IRecordMetaData[], storage: Vault await platform.unwrapKeys(recordKeys, storage) } -const processBreachWatchRecords = async (bwRecords: IBreachWatchRecord[], storage: VaultStorage) => { +const processBreachWatchRecords = async (storage: VaultStorage, bwRecords?: IBreachWatchRecord[] | null) => { + if (!bwRecords) return for (const bwRecord of bwRecords as NN[]) { if (!bwRecord.recordUid) continue @@ -1016,7 +1041,11 @@ const processBreachWatchRecords = async (bwRecords: IBreachWatchRecord[], storag } } -const processBreachWatchSecurityData = async (securityData: IBreachWatchSecurityData[], storage: VaultStorage) => { +const processBreachWatchSecurityData = async ( + storage: VaultStorage, + securityData?: IBreachWatchSecurityData[] | null +) => { + if (!securityData) return for (const bwSecurityData of securityData as NN[]) { const uid = webSafe64FromBytes(bwSecurityData.recordUid) @@ -1032,7 +1061,11 @@ const processBreachWatchSecurityData = async (securityData: IBreachWatchSecurity } } -const processSecurityScoreData = async (securityScoreDataList: Vault.ISecurityScoreData[], storage: VaultStorage) => { +const processSecurityScoreData = async ( + storage: VaultStorage, + securityScoreDataList?: Vault.ISecurityScoreData[] | null +) => { + if (!securityScoreDataList) return for (const securityScoreData of securityScoreDataList) { if (!securityScoreData.recordUid || typeof securityScoreData.revision !== 'number') continue @@ -1058,6 +1091,388 @@ const processSecurityScoreData = async (securityScoreDataList: Vault.ISecuritySc } } +// Keeper Drive Processors Start + +const processKdRemovedRecordLinks = async ( + removedDependencies: RemovedDependencies, + keeperDriveRemovedRecordLinks?: Vault.IRecordLink[] | null +) => { + if (!keeperDriveRemovedRecordLinks) return + for (const link of keeperDriveRemovedRecordLinks) { + if (!link.childRecordUid || !link.parentRecordUid) continue + const childRecordUid = webSafe64FromBytes(link.childRecordUid) + const parentRecordUid = webSafe64FromBytes(link.parentRecordUid) + addRemovedDependencies(removedDependencies, parentRecordUid, childRecordUid) + } +} + +const processKdRecordLinks = async (storage: VaultStorage, keeperDriveRecordLinks?: Vault.IRecordLink[] | null) => { + if (!keeperDriveRecordLinks) return + for (const link of keeperDriveRecordLinks) { + if (!link.childRecordUid || !link.parentRecordUid || !link.recordKey) continue + const childRecordUid = webSafe64FromBytes(link.childRecordUid) + const parentRecordUid = webSafe64FromBytes(link.parentRecordUid) + + try { + await platform.unwrapKey(link.recordKey, childRecordUid, parentRecordUid, 'gcm', 'aes', storage, true) + await storage.put({ + kind: 'keeper_drive_record_link', + childRecordUid, + parentRecordUid, + }) + } catch (e: any) { + console.error( + `[ks] The record link for ${childRecordUid} cannot be decrypted by ${parentRecordUid} (${e.message})` + ) + } + } +} + +const processKdRevokedFolderAccesses = async ( + storage: VaultStorage, + keeperDriveRevokedFolderAccesses?: Folder.IRevokedAccess[] | null +) => { + if (!keeperDriveRevokedFolderAccesses) return + for (const revokedAccess of keeperDriveRevokedFolderAccesses) { + if (!revokedAccess.actorUid || !revokedAccess.folderUid) continue + const actorUid = webSafe64FromBytes(revokedAccess.actorUid) + const folderUid = webSafe64FromBytes(revokedAccess.folderUid) + await storage.delete('keeper_drive_folder_access', createKdFolderAccessCompositeKey(actorUid, folderUid), { + kind: 'keeper_drive_folder_access', + actorUid, + folderUid, + }) + } +} + +const processKdRemovedFolders = async ( + storage: VaultStorage, + keeperDriveRemovedFolders?: Folder.IFolderRemoved[] | null +) => { + if (!keeperDriveRemovedFolders) return + for (const removedFolder of keeperDriveRemovedFolders) { + if (!removedFolder.folderUid) continue + const folderUid = webSafe64FromBytes(removedFolder.folderUid) + await storage.delete('keeper_drive_folder', folderUid) + } +} + +const processKdFolderKeys = async (storage: VaultStorage, folderKeys?: Folder.IFolderKey[] | null) => { + if (!folderKeys) return + const encryptedByDataKeyMap: UnwrapKeyMap = {} + const encryptedByParentKeyMap: UnwrapKeyMap = {} + const encryptedByDataKey = folderKeys.filter( + (key) => key.encryptedBy === Folder.FolderKeyEncryptionType.ENCRYPTED_BY_USER_KEY + ) + const encryptedByParentKey = folderKeys.filter( + (key) => key.encryptedBy === Folder.FolderKeyEncryptionType.ENCRYPTED_BY_PARENT_KEY + ) + for (const key of encryptedByDataKey) { + if (!key.folderUid || !key.folderKey || !key.parentUid || isNil(key.encryptedBy)) continue + const folderUid = webSafe64FromBytes(key.folderUid) + const parentUid = webSafe64FromBytes(key.parentUid) + encryptedByDataKeyMap[folderUid] = { + data: key.folderKey, + dataId: folderUid, + keyId: 'data', + encryptionType: 'gcm', + unwrappedType: 'aes', + } + } + await platform.unwrapKeys(encryptedByDataKeyMap, storage) + for (const key of encryptedByParentKey) { + if (!key.folderUid || !key.folderKey || !key.parentUid || isNil(key.encryptedBy)) continue + const folderUid = webSafe64FromBytes(key.folderUid) + const parentUid = webSafe64FromBytes(key.parentUid) + encryptedByParentKeyMap[folderUid] = { + data: key.folderKey, + dataId: folderUid, + keyId: parentUid, + encryptionType: 'gcm', + unwrappedType: 'aes', + } + } + await platform.unwrapKeys(encryptedByParentKeyMap, storage) +} + +const processKdFolderAccesses = async ( + storage: VaultStorage, + keeperDriveFolderAccesses?: Folder.IFolderAccessData[] | null +) => { + if (!keeperDriveFolderAccesses) return + const folderKeyMap: UnwrapKeyMap = {} + for (const folderAccess of keeperDriveFolderAccesses) { + const folderUid = webSafe64FromBytes(folderAccess.folderUid!) + if ( + !folderAccess.folderUid || + !folderAccess.accessTypeUid || + !folderAccess.accessRoleType || + !folderAccess.accessType || + !folderAccess.permissions + ) + continue + const accessTypeUid = webSafe64FromBytes(folderAccess.accessTypeUid) + const permission = folderAccess.permissions + // for child folders, they're only encrypted by their parent folder key + if ( + folderAccess.folderKey && + folderAccess.folderKey.encryptedKey && + !isNil(folderAccess.folderKey.encryptedKeyType) + ) { + const keyInfo = + folderAccess.accessType === Folder.AccessType.AT_USER + ? mapKeyType(folderAccess.folderKey.encryptedKeyType) + : folderAccess.accessType === Folder.AccessType.AT_TEAM + ? mapTeamKeyType(folderAccess.folderKey.encryptedKeyType, accessTypeUid) + : null + if (!keyInfo) continue + folderKeyMap[folderUid] = { + data: folderAccess.folderKey.encryptedKey, + dataId: folderUid, + keyId: keyInfo.keyId, + encryptionType: keyInfo.encryptionType, + unwrappedType: 'aes', + } + } + await storage.put({ + kind: 'keeper_drive_folder_access', + accessUid: createKdFolderAccessCompositeKey(accessTypeUid, folderUid), + folderUid, + accessTypeUid, + accessType: folderAccess.accessType, + accessRoleType: folderAccess.accessRoleType, + permission, + }) + } + await platform.unwrapKeys(folderKeyMap, storage) +} + +const processKdFolderSharingState = async ( + storage: VaultStorage, + keeperDriveFolderSharingStates?: Vault.IFolderSharingState[] | null +) => { + if (!keeperDriveFolderSharingStates) return + for (const folderSharingState of keeperDriveFolderSharingStates) { + if (!folderSharingState.folderUid) continue + await storage.put({ + kind: 'keeper_drive_folder_sharing_state', + folderUid: webSafe64FromBytes(folderSharingState.folderUid), + shared: toOptional(folderSharingState.shared), + count: toOptional(folderSharingState.count), + }) + } +} + +const processKdFolders = async (storage: VaultStorage, keeperDriveFolders?: Folder.IFolderData[] | null) => { + if (!keeperDriveFolders) return + for (const folder of keeperDriveFolders || []) { + if ( + !folder.folderUid || + !folder.folderKey || + !folder.data || + !folder.ownerInfo || + folder.type !== Folder.FolderUsageType.UT_NORMAL + ) + continue + const folderUid = webSafe64FromBytes(folder.folderUid) + const parentUid = + folder.parentUid && folder.parentUid.length > 0 ? webSafe64FromBytes(folder.parentUid) : undefined + try { + const folderData = JSON.parse( + platform.bytesToString(await platform.decrypt(folder.data, folderUid, 'gcm', storage)) + ) + await storage.put({ + kind: 'keeper_drive_folder', + uid: folderUid, + data: folderData, + parentUid, + ownerInfo: { + accountUid: folder.ownerInfo.accountUid + ? webSafe64FromBytes(folder.ownerInfo.accountUid) + : undefined, + username: toOptional(folder.ownerInfo.username), + }, + lastModified: toOptional(folder.lastModified), + type: toOptional(folder.type), + inheritUserPermissions: toOptional(folder.inheritUserPermissions), + }) + } catch (err: any) { + console.error(`[ks] folder ${folderUid} cannot be decrypted (${err.message})`) + } + } +} + +const processKdRecordAccess = async (storage: VaultStorage, kdRecordAccesses?: Folder.IRecordAccessData[] | null) => { + if (!kdRecordAccesses) return + for (const recordAccess of kdRecordAccesses) { + if ( + !recordAccess.recordUid || + !recordAccess.accessTypeUid || + isNil(recordAccess.accessType) || + isNil(recordAccess.accessRoleType) + ) + continue + const recordUid = webSafe64FromBytes(recordAccess.recordUid) + const accessTypeUid = webSafe64FromBytes(recordAccess.accessTypeUid) + await storage.put({ + kind: 'keeper_drive_record_access', + accessUid: createKdRecordAccessCompositeKey(accessTypeUid, recordUid), + accessTypeUid, + accessType: recordAccess.accessType, + recordUid, + accessRoleType: recordAccess.accessRoleType, + owner: toOptional(recordAccess.owner), + inherited: toOptional(recordAccess.inherited), + hidden: toOptional(recordAccess.hidden), + deniedAccess: toOptional(recordAccess.deniedAccess), + canEdit: toOptional(recordAccess.canEdit), + canView: toOptional(recordAccess.canView), + canListAccess: toOptional(recordAccess.canListAccess), + canUpdateAccess: toOptional(recordAccess.canUpdateAccess), + canDelete: toOptional(recordAccess.canDelete), + canChangeOwnership: toOptional(recordAccess.canChangeOwnership), + canRequestAccess: toOptional(recordAccess.canRequestAccess), + canApproveAccess: toOptional(recordAccess.canApproveAccess), + dateCreated: toOptional(recordAccess.dateCreated), + lastModified: toOptional(recordAccess.lastModified), + tlaProperties: toOptional(recordAccess.tlaProperties), + }) + } +} + +const processKdFolderRecords = async ( + storage: VaultStorage, + dependencies: Dependencies, + kdFolderRecords?: Folder.IFolderRecord[] | null +) => { + if (!kdFolderRecords) return + const recordKeyMap: UnwrapKeyMap = {} + for (const folderRecord of kdFolderRecords) { + if ( + !folderRecord.folderUid || + !folderRecord.recordMetadata || + !folderRecord.recordMetadata.recordUid || + !folderRecord.recordMetadata.encryptedRecordKey || + isNil(folderRecord.recordMetadata.encryptedRecordKeyType) || + isNil(folderRecord.folderKeyEncryptionType) + ) + continue + const folderUid = webSafe64FromBytes(folderRecord.folderUid) + const recordUid = webSafe64FromBytes(folderRecord.recordMetadata.recordUid) + const keyInfo = mapKeyType(folderRecord.recordMetadata.encryptedRecordKeyType) + if (!keyInfo) continue + recordKeyMap[recordUid] = { + data: folderRecord.recordMetadata.encryptedRecordKey, + dataId: recordUid, + keyId: + folderRecord.folderKeyEncryptionType === Folder.FolderKeyEncryptionType.ENCRYPTED_BY_PARENT_KEY + ? folderUid + : keyInfo.keyId, + encryptionType: keyInfo?.encryptionType, + unwrappedType: 'aes', + } + if (folderUid) { + addDependencies(dependencies, folderUid, recordUid, 'record') + await storage.put({ + kind: 'keeper_drive_folder_record', + folderUid, + recordUid, + }) + } + } + await platform.unwrapKeys(recordKeyMap, storage) +} + +const processKdRecordSharingStates = async ( + storage: VaultStorage, + kdRecordSharingStates?: record.v3.sharing.IRecordSharingState[] | null +) => { + if (!kdRecordSharingStates) return + for (const sharingState of kdRecordSharingStates) { + if (!sharingState.recordUid) continue + await storage.put({ + kind: 'keeper_drive_record_sharing_state', + recordUid: webSafe64FromBytes(sharingState.recordUid), + isDirectlyShared: toOptional(sharingState.isDirectlyShared), + isIndirectlyShared: toOptional(sharingState.isIndirectlyShared), + isShared: toOptional(sharingState.isShared), + }) + } +} + +const processKdRecords = async ( + storage: VaultStorage, + kdRecordData?: Folder.IRecordData[] | null, + kdRecords?: Vault.IDriveRecord[] | null +) => { + if (!kdRecordData || !kdRecords) return + const kdRecordMap: { + [key in string]: Vault.IDriveRecord + } = {} + for (const record of kdRecords) { + if (!record.recordUid || !record.revision) continue + kdRecordMap[webSafe64FromBytes(record.recordUid)] = record + } + for (const record of kdRecordData) { + if (!record.recordUid || !record.data) continue + const recordUid = webSafe64FromBytes(record.recordUid) + const metadata = kdRecordMap[recordUid] || {} + if (!metadata.version) continue + try { + const decryptedData = await platform.decrypt(record.data, recordUid, 'gcm', storage) + const recordData = JSON.parse(platform.bytesToString(decryptedData)) + await storage.put({ + kind: 'record', + uid: recordUid, + data: recordData, + version: metadata.version, + revision: metadata.revision as number, + shared: !!metadata.shared, + clientModifiedTime: metadata.clientModifiedTime as number, + isKeeperDriveData: true, + }) + } catch (err: any) { + console.error(`[kd] record ${recordUid} cannot be decrypted: ${err.message}`) + } + } +} + +const processKdRemovedFolderRecords = ( + removedDependencies: RemovedDependencies, + keeperDriveRemovedFolderRecords?: Records.IFolderRecordKey[] | null +) => { + if (!keeperDriveRemovedFolderRecords) return + for (const folderRecord of keeperDriveRemovedFolderRecords) { + if (!folderRecord.recordUid || !folderRecord.folderUid) continue + const recordUid = webSafe64FromBytes(folderRecord.recordUid) + const folderUid = webSafe64FromBytes(folderRecord.folderUid) + addRemovedDependencies(removedDependencies, folderUid, { + parentKind: 'keeper_drive_folder', + childKind: 'record', + childUid: recordUid, + }) + } +} + +const processKdRevokedRecordAccesses = async ( + storage: VaultStorage, + kdRevokedAccesses?: record.v3.sharing.IRevokedAccess[] | null +) => { + if (!kdRevokedAccesses) return + for (const revokedAccess of kdRevokedAccesses) { + if (!revokedAccess.actorUid || !revokedAccess.recordUid) continue + const actorUid = webSafe64FromBytes(revokedAccess.actorUid) + const recordUid = webSafe64FromBytes(revokedAccess.recordUid) + await storage.delete('keeper_drive_record_access', createKdRecordAccessCompositeKey(actorUid, recordUid), { + kind: 'keeper_drive_record_access', + actorUid, + recordUid, + }) + } +} + +// Keeper Drive Processors End + export type SyncLogFormat = '!' | 'raw' | 'obj' | 'str' | 'cnt' | 'cnt_t' const logProtobuf = (data: any, format: SyncLogFormat, seqNo: number, counts: any) => { @@ -1087,15 +1502,20 @@ const logProtobuf = (data: any, format: SyncLogFormat, seqNo: number, counts: an } const getCounts = (obj: Vault.ISyncDownResponse): SyncResponseCounts => { - const results = {} - for (const prop in obj) { - if (['continuationToken', 'constructor'].includes(prop)) { - continue - } - if (obj[prop]?.length) { - results[prop] = obj[prop].length + const results: Record = {} + const collect = (source: any) => { + if (!source) return + for (const prop in source) { + if (['continuationToken', 'constructor', 'keeperDriveData'].includes(prop)) { + continue + } + if (source[prop]?.length) { + results[prop] = (results[prop] || 0) + source[prop].length + } } } + collect(obj) + collect(obj.keeperDriveData) return results } @@ -1221,6 +1641,7 @@ export const syncDown = async (options: SyncDownOptions): Promise => } result.pageCount += 1 networkTime += requestTime + const keeperDriveData = resp.keeperDriveData ?? {} const dependencies = {} await processUsers(resp.users, storage) @@ -1251,7 +1672,7 @@ export const syncDown = async (options: SyncDownOptions): Promise => await processRecordRotations(resp.recordRotations, storage) - await processNonSharedData(resp.nonSharedData, storage) + await processNonSharedData(storage, resp.nonSharedData) await processSharedFolderFolders(resp.sharedFolderFolders, storage, dependencies) @@ -1261,11 +1682,37 @@ export const syncDown = async (options: SyncDownOptions): Promise => await processProfilePic(resp.profilePic, storage) - await processBreachWatchRecords(resp.breachWatchRecords, storage) + await processBreachWatchRecords(storage, resp.breachWatchRecords) - await processBreachWatchSecurityData(resp.breachWatchSecurityData, storage) + await processBreachWatchSecurityData(storage, resp.breachWatchSecurityData) - await processSecurityScoreData(resp.securityScoreData, storage) + await processSecurityScoreData(storage, resp.securityScoreData) + + await processKdFolderAccesses(storage, keeperDriveData.folderAccesses) + + await processKdFolderKeys(storage, keeperDriveData.folderKeys) + + await processKdFolders(storage, keeperDriveData.folders) + + await processKdFolderSharingState(storage, keeperDriveData.folderSharingState) + + await processKdRecordAccess(storage, keeperDriveData.recordAccesses) + + await processKdFolderRecords(storage, dependencies, keeperDriveData.folderRecords) + + await processKdRecordLinks(storage, keeperDriveData.recordLinks) + + await processKdRecords(storage, keeperDriveData.recordData, keeperDriveData.records) + + await processNonSharedData(storage, keeperDriveData.nonSharedData) + + await processKdRecordSharingStates(storage, keeperDriveData.recordSharingStates) + + await processBreachWatchRecords(storage, keeperDriveData.breachWatchRecords) + + await processBreachWatchSecurityData(storage, keeperDriveData.breachWatchSecurityData) + + await processSecurityScoreData(storage, keeperDriveData.securityScoreData) await storage.addDependencies(dependencies) @@ -1325,6 +1772,16 @@ export const syncDown = async (options: SyncDownOptions): Promise => await storage.delete('user', user) } + await processKdRemovedFolders(storage, keeperDriveData.removedFolders) + + await processKdRevokedFolderAccesses(storage, keeperDriveData.revokedFolderAccesses) + + processKdRemovedFolderRecords(removedDependencies, keeperDriveData.removedFolderRecords) + + await processKdRevokedRecordAccesses(storage, keeperDriveData.revokedRecordAccesses) + + await processKdRemovedRecordLinks(removedDependencies, keeperDriveData.removedRecordLinks) + await storage.removeDependencies(removedDependencies) continuationToken = resp.continuationToken || undefined From 39dc3dc7510ba02ce7df86c384b6cb9d3921ab27 Mon Sep 17 00:00:00 2001 From: Hoseong Lee <154545063+hleekeeper@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:45:04 -0500 Subject: [PATCH 08/14] v17.2.5 (#166) --- keeperapi/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keeperapi/package-lock.json b/keeperapi/package-lock.json index 0f97039..ac0bb86 100644 --- a/keeperapi/package-lock.json +++ b/keeperapi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@keeper-security/keeperapi", - "version": "17.2.3", + "version": "17.2.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@keeper-security/keeperapi", - "version": "17.2.3", + "version": "17.2.5", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", From d223bdd6506ab7d3c8df8b768b43ab8625f869e6 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Wed, 10 Jun 2026 22:24:11 +0530 Subject: [PATCH 09/14] Roles crud operation implementation (#163) --- KeeperSdk/src/roles/roleUtils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/KeeperSdk/src/roles/roleUtils.ts b/KeeperSdk/src/roles/roleUtils.ts index ab8519c..5e3c503 100644 --- a/KeeperSdk/src/roles/roleUtils.ts +++ b/KeeperSdk/src/roles/roleUtils.ts @@ -182,10 +182,17 @@ export async function applyRoleEnforcements( continue } + const exists = roleHasEnforcement(roleId, enforcement, existingLinks) const response = await auth.executeRestCommand( - roleEnforcementAddCommand({ role_id: roleId, enforcement, value }) + exists + ? roleEnforcementUpdateCommand({ role_id: roleId, enforcement, value }) + : roleEnforcementAddCommand({ role_id: roleId, enforcement, value }) + ) + assertCommandSucceeded( + response, + `role_enforcement_${exists ? 'update' : 'add'} failed: ${enforcement}`, + ResultCodes.ROLE_ENFORCEMENT_FAILED ) - assertCommandSucceeded(response, `role_enforcement_add failed: ${enforcement}`, ResultCodes.ROLE_ENFORCEMENT_FAILED) } } From 5c7d22f8cc109ca6df22c0db90608a2146d2aa46 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 11 Jun 2026 21:50:23 +0530 Subject: [PATCH 10/14] Rest message for keeper drive and enterprise roles (#168) --- keeperapi/package-lock.json | 4 ++-- keeperapi/src/restMessages.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/keeperapi/package-lock.json b/keeperapi/package-lock.json index ac0bb86..9978f9f 100644 --- a/keeperapi/package-lock.json +++ b/keeperapi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@keeper-security/keeperapi", - "version": "17.2.5", + "version": "17.2.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@keeper-security/keeperapi", - "version": "17.2.5", + "version": "17.2.6", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index 2a36bef..6b32762 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -968,3 +968,13 @@ export const keeperDriveRecordsUpdate = ( data: Records.IRecordsUpdateRequest ): RestMessage => createMessage(data, '/vault/records/v3/update', Records.RecordsUpdateRequest, Records.RecordsModifyResponse) + +export const getSharingAdminsMessage = ( + data: Enterprise.IGetSharingAdminsRequest +): RestMessage => + createMessage( + data, + 'enterprise/get_sharing_admins', + Enterprise.GetSharingAdminsRequest, + Enterprise.GetSharingAdminsResponse + ) From a1509e635b9161c52be3dfee00f7f111ce4ff491 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Mon, 8 Jun 2026 19:16:17 +0530 Subject: [PATCH 11/14] Handle existing role enforcements with update instead of add (#164) --- keeperapi/src/commands.ts | 43 --------------------------------------- 1 file changed, 43 deletions(-) diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index 4b9bff2..011c1d3 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -189,49 +189,6 @@ export const roleEnforcementAddCommand = ( request: RoleEnforcementAddRequest ): RestCommand => createCommand(request, 'role_enforcement_add') -export const roleEnforcementUpdateCommand = ( - request: RoleEnforcementAddRequest -): RestCommand => createCommand(request, 'role_enforcement_update') - -export type RoleEnforcementRemoveRequest = { - role_id: number - enforcement: string -} - -export const roleEnforcementRemoveCommand = ( - request: RoleEnforcementRemoveRequest -): RestCommand => createCommand(request, 'role_enforcement_remove') - -export type EnterpriseAllocateIdsRequest = { - number_requested: number -} - -export const enterpriseAllocateIdsCommand = ( - request: EnterpriseAllocateIdsRequest -): RestCommand => - createCommand(request, 'enterprise_allocate_ids') - -export type RoleEditRequest = { - role_id: number - node_id: number - encrypted_data: string - visible_below: boolean - new_user_inherit: boolean -} - -export const roleAddCommand = (request: RoleEditRequest): RestCommand => - createCommand(request, 'role_add') - -export const roleUpdateCommand = (request: RoleEditRequest): RestCommand => - createCommand(request, 'role_update') - -export type RoleDeleteRequest = { - role_id: number -} - -export const roleDeleteCommand = (request: RoleDeleteRequest): RestCommand => - createCommand(request, 'role_delete') - export type MoveRequest = { to_type: 'user_folder' | 'shared_folder' | 'shared_folder_folder' to_uid?: string From eb015bcaa4f0165f46e46a8bcb5ed9c03860821c Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Tue, 9 Jun 2026 13:48:46 +0530 Subject: [PATCH 12/14] Updated default client version --- KeeperSdk/src/utils/constants.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index 24d29f8..b33a1b8 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -1,8 +1,13 @@ +const DEFAULT_CLIENT_VERSION = 'c18.0.0' +const DEFAULT_DEVICE_NAME = 'JavaScript Keeper SDK' +const DEFAULT_CONFIG_DIR = '.keeper' +const DEFAULT_LOG_FORMAT = '!' + export const SdkDefaults = { - CLIENT_VERSION: 'c17.0.0', - DEVICE_NAME: 'JavaScript Keeper SDK', - CONFIG_DIR: '.keeper', - LOG_FORMAT: '!', + CLIENT_VERSION: DEFAULT_CLIENT_VERSION, + DEVICE_NAME: DEFAULT_DEVICE_NAME, + CONFIG_DIR: DEFAULT_CONFIG_DIR, + LOG_FORMAT: DEFAULT_LOG_FORMAT, } as const export const AuthDefaults = { From 528e8738bc9b5ff74c6f13c058fcd25de4d257d1 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Wed, 10 Jun 2026 15:37:33 +0530 Subject: [PATCH 13/14] kd list and get implementation --- KeeperSdk/src/index.ts | 45 ++ .../NestedShareFolderManager.ts | 75 +++ KeeperSdk/src/nestedShareFolders/getNsf.ts | 527 ++++++++++++++++++ KeeperSdk/src/nestedShareFolders/index.ts | 54 ++ KeeperSdk/src/nestedShareFolders/listNsf.ts | 172 ++++++ .../src/nestedShareFolders/nsfHelpers.ts | 194 +++++++ KeeperSdk/src/sharing/Sharing.ts | 12 +- KeeperSdk/src/storage/InMemoryStorage.ts | 8 + KeeperSdk/src/utils/constants.ts | 7 + KeeperSdk/src/utils/index.ts | 1 + KeeperSdk/src/vault/KeeperVault.ts | 34 ++ examples/sdk_example/package.json | 2 + .../src/nestedShareFolders/get_nsf.ts | 54 ++ .../src/nestedShareFolders/list_nsf.ts | 69 +++ keeperapi/src/restMessages.ts | 30 + 15 files changed, 1275 insertions(+), 9 deletions(-) create mode 100644 KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts create mode 100644 KeeperSdk/src/nestedShareFolders/getNsf.ts create mode 100644 KeeperSdk/src/nestedShareFolders/index.ts create mode 100644 KeeperSdk/src/nestedShareFolders/listNsf.ts create mode 100644 KeeperSdk/src/nestedShareFolders/nsfHelpers.ts create mode 100644 examples/sdk_example/src/nestedShareFolders/get_nsf.ts create mode 100644 examples/sdk_example/src/nestedShareFolders/list_nsf.ts diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 5e93ce1..ca5c06c 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -25,6 +25,7 @@ export { extractResultCode, SdkDefaults, AuthDefaults, + NsfErrorCode, ResultCodes, AuthErrorCode, SessionErrorCode, @@ -443,6 +444,50 @@ export { export { UserManager } from './users/UserManager' +export { + ROOT_FOLDER_UID, + KeeperDriveKind, + NsfItemType, + formatAccessRoleType, + formatAccessType, + normalizeParentUid, + isRootFolderUid, + getKeeperDriveFolders, + getKeeperDriveRecords, + findRecordFolderLocation, + buildFolderPath, + isSensitiveFieldType, + ListNsfFormat, + listNestedShareFolders, + formatListNsfTable, + renderListNsfAsciiTable, + formatListNsfCsv, + formatListNsfJson, + formatListNsfOutput, + GetNsfFormat, + resolveNsfFolder, + resolveNsfRecord, + getNestedShareFolder, + formatNsfFolderDetail, + formatNsfRecordDetail, + formatNsfDetail, + NestedShareFolderManager, +} from './nestedShareFolders' +export type { + ListNsfFormatInput, + ListNsfOptions, + ListNsfRow, + FormattedListNsfTable, + GetNsfFormatInput, + GetNsfOptions, + GetNsfResult, + NsfFolderView, + NsfRecordView, + NsfFolderPermission, + NsfFolderAccessRow, + NsfRecordPermission, +} from './nestedShareFolders' + export type { DRecord, DRecordMetadata, diff --git a/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts b/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts new file mode 100644 index 0000000..bf1169b --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts @@ -0,0 +1,75 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { KeeperSdkError, ResultCodes } from '../utils' +import { + formatListNsfOutput, + formatListNsfTable, + listNestedShareFolders, + renderListNsfAsciiTable, + type FormattedListNsfTable, + type ListNsfFormatInput, + type ListNsfOptions, + type ListNsfRow, +} from './listNsf' +import { + formatNsfDetail as renderNsfDetail, + formatNsfFolderDetail as renderNsfFolderDetail, + formatNsfRecordDetail as renderNsfRecordDetail, + getNestedShareFolder, + type GetNsfOptions, + type GetNsfResult, + type NsfFolderView, + type NsfRecordView, +} from './getNsf' + +export type AuthProvider = () => Auth + +export class NestedShareFolderManager { + 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 listNestedShareFolders(options: ListNsfOptions = {}): ListNsfRow[] { + return listNestedShareFolders(this.storage, options) + } + + public formatListNsfTable(rows: ListNsfRow[], options: { columnWidth?: number } = {}): FormattedListNsfTable { + return formatListNsfTable(rows, options) + } + + public renderListNsfAsciiTable(table: FormattedListNsfTable, options: { minColWidth?: number } = {}): string { + return renderListNsfAsciiTable(table, options) + } + + public formatListNsfOutput(rows: ListNsfRow[], format: ListNsfFormatInput = 'table'): string { + return formatListNsfOutput(rows, format) + } + + public async getNestedShareFolder(identifier: string, options: GetNsfOptions = {}): Promise { + return getNestedShareFolder(this.storage, this.requireAuth(), identifier, options) + } + + public formatNsfDetail(result: GetNsfResult, verbose = false): string { + return renderNsfDetail(result, verbose) + } + + public formatNsfFolderDetail(view: NsfFolderView, verbose = false): string { + return renderNsfFolderDetail(view, verbose) + } + + public formatNsfRecordDetail(view: NsfRecordView, verbose = false): string { + return renderNsfRecordDetail(view, verbose) + } +} diff --git a/KeeperSdk/src/nestedShareFolders/getNsf.ts b/KeeperSdk/src/nestedShareFolders/getNsf.ts new file mode 100644 index 0000000..cf92712 --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/getNsf.ts @@ -0,0 +1,527 @@ +import type { Auth, DRecord, DKdFolder, DKdFolderAccess } from '@keeper-security/keeperapi' +import { + Enterprise, + Folder, + Records, + getRecordsDetailsMessage, + normal64Bytes, + webSafe64FromBytes, +} from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { + getRecordFields, + getRecordLogin, + getRecordPassword, + getRecordTitle, + getRecordType, + getRecordUrl, +} from '../records/RecordUtils' +import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../utils' +import { + buildFolderPath, + collectRecordsInFolder, + findRecordFolderLocation, + folderAccessDisplayRole, + formatAccessType, + getFolderAccessEntries, + getKeeperDriveFolder, + getKeeperDriveFolders, + getKeeperDriveRecord, + getKeeperDriveRecords, + isFolderShareAdministrator, + isFolderUserPermission, + isSensitiveFieldType, + normalizeParentUid, + resolveAccessUsername, +} from './nsfHelpers' + +const MASKED_VALUE = '********' +const FOLDER_LABEL_WIDTH = 22 +const RECORD_LABEL_WIDTH = 17 +const TOP_LEVEL_FIELD_TYPES = new Set(['login', 'password', 'url', 'note', 'multiline', 'text']) +const UNKNOWN_RECORD_TITLES = new Set(['(no data)', '(untitled)', 'Unknown']) +const FOLDER_USER_PERMISSIONS_HEADING = ' User Permissions:' +const FOLDER_SHARE_ADMINS_HEADING = ' Share Administrators:' +const RECORD_USER_PERMISSIONS_HEADING = 'User Permissions:' +const SHARING_ADMINS_API_PATH = 'enterprise/get_sharing_admins' + +export enum GetNsfFormat { + Detail = 'detail', + JSON = 'json', +} + +export type GetNsfFormatInput = GetNsfFormat | `${GetNsfFormat}` + +export type GetNsfOptions = { + format?: GetNsfFormatInput + verbose?: boolean + unmask?: boolean +} + +export type NsfFolderAccessRow = { + username: string + role: string +} + +export type NsfFolderPermission = { + accessTypeUid: string + accessType: string + accessRoleType: string + inherited?: boolean + hidden?: boolean + canAdd?: boolean + canRemove?: boolean + canDelete?: boolean + canListAccess?: boolean + canUpdateAccess?: boolean + canEditRecords?: boolean + canViewRecords?: boolean + canListRecords?: boolean +} + +export type NsfRecordPermission = { + username: string + accountUid?: string + owner: boolean + shareAdmin: boolean + shareable: boolean + editable: boolean + awaitingApproval: boolean + expiration?: number +} + +export type NsfFolderView = { + objectType: 'folder' + folderUid: string + name: string + parentUid: string + path: string + userPermissions: NsfFolderAccessRow[] + shareAdmins: NsfFolderAccessRow[] + teamPermissions: NsfFolderPermission[] + records: { uid: string; title: string; type: string }[] +} + +export type NsfRecordView = { + objectType: 'record' + recordUid: string + title: string + type: string + revision: number + version: number + folderLocation: string + login?: string + password?: string + url?: string + notes?: string + fields: { type: string; label?: string; value: string }[] + userPermissions: NsfRecordPermission[] + shareAdmins: string[] +} + +export type GetNsfResult = { kind: 'folder'; view: NsfFolderView } | { kind: 'record'; view: NsfRecordView } + +function folderDetailRow(label: string, value: string): string { + return `${label.padStart(FOLDER_LABEL_WIDTH)}: ${value}` +} + +function recordDetailRow(label: string, value: string): string { + return `${label.padStart(RECORD_LABEL_WIDTH)}: ${value}` +} + +function recordDetailsMessage(recordUid: string, include: Records.RecordDetailsInclude) { + return getRecordsDetailsMessage({ + clientTime: Date.now(), + recordUid: [normal64Bytes(recordUid)], + recordDetailsInclude: include, + }) +} + +function recordSharingAdminsMessage(recordUid: string) { + const uid = normal64Bytes(recordUid) + return { + path: SHARING_ADMINS_API_PATH, + toBytes(): Uint8Array { + return Enterprise.GetSharingAdminsRequest.encode({ recordUid: uid }).finish() + }, + fromBytes(data: Uint8Array): Enterprise.IGetSharingAdminsResponse { + return Enterprise.GetSharingAdminsResponse.decode(data) + }, + } +} + +function bytesToUid(bytes: Uint8Array | null | undefined): string | undefined { + return bytes?.length ? webSafe64FromBytes(bytes) : undefined +} + +function longToNumber(value: number | { toNumber: () => number } | null | undefined): number | undefined { + if (value == null) return undefined + return typeof value === 'number' ? value : value.toNumber() +} + +function resolveByUidOrName( + items: T[], + identifier: string, + getUid: (item: T) => string, + getName: (item: T) => string +): T | undefined { + const trimmed = identifier.trim() + if (!trimmed) return undefined + + const byUid = items.find((item) => getUid(item) === trimmed) + if (byUid) return byUid + + const lower = trimmed.toLowerCase() + const nameMatches = items.filter((item) => getName(item).toLowerCase() === lower) + if (nameMatches.length === 1) return nameMatches[0] + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Multiple matches found for "${identifier}". Use a UID instead.`, + ResultCodes.MULTIPLE_NSF_MATCHES + ) + } + return undefined +} + +function resolveRecordByTitleSearch(storage: InMemoryStorage, identifier: string): DRecord | undefined { + const lower = identifier.toLowerCase() + const matches = getKeeperDriveRecords(storage).filter((record) => { + const title = getRecordTitle(record) + return title && lower.length > 0 && title.toLowerCase().includes(lower) + }) + if (matches.length === 1) return matches[0] + if (matches.length > 1) { + throw new KeeperSdkError( + `Multiple records matched "${identifier}". Use a UID instead.`, + ResultCodes.MULTIPLE_NSF_MATCHES + ) + } + return undefined +} + +function mapFolderPermission(entry: DKdFolderAccess): NsfFolderPermission { + const permission = entry.permission + return { + accessTypeUid: entry.accessTypeUid, + accessType: formatAccessType(entry.accessType), + accessRoleType: folderAccessDisplayRole(entry), + inherited: entry.inherited, + hidden: entry.hidden, + canAdd: permission?.canAdd ?? undefined, + canRemove: permission?.canRemove ?? undefined, + canDelete: permission?.canDelete ?? undefined, + canListAccess: permission?.canListAccess ?? undefined, + canUpdateAccess: permission?.canUpdateAccess ?? undefined, + canEditRecords: permission?.canEditRecords ?? undefined, + canViewRecords: permission?.canViewRecords ?? undefined, + canListRecords: permission?.canListRecords ?? undefined, + } +} + +function buildFolderAccessRow( + storage: InMemoryStorage, + folder: DKdFolder, + entry: DKdFolderAccess +): NsfFolderAccessRow { + return { + username: resolveAccessUsername(storage, entry.accessTypeUid, folder), + role: folderAccessDisplayRole(entry), + } +} + +function splitFolderPermissions(storage: InMemoryStorage, folder: DKdFolder) { + const entries = getFolderAccessEntries(storage, folder.uid) + return { + userPermissions: entries + .filter(isFolderUserPermission) + .map((entry) => buildFolderAccessRow(storage, folder, entry)), + shareAdmins: entries + .filter(isFolderShareAdministrator) + .map((entry) => buildFolderAccessRow(storage, folder, entry)), + teamPermissions: entries + .filter((entry) => entry.accessType === Folder.AccessType.AT_TEAM) + .map(mapFolderPermission), + } +} + +function prependOwnerRow(rows: NsfFolderAccessRow[], ownerUsername: string): NsfFolderAccessRow[] { + if (rows.some((entry) => entry.username === ownerUsername)) return rows + return [{ username: ownerUsername, role: 'owner' }, ...rows] +} + +function ensureFolderOwnerListed( + folder: DKdFolder, + userPermissions: NsfFolderAccessRow[], + shareAdmins: NsfFolderAccessRow[] +): { userPermissions: NsfFolderAccessRow[]; shareAdmins: NsfFolderAccessRow[] } { + const ownerUsername = folder.ownerInfo?.username?.trim() + if (!ownerUsername) return { userPermissions, shareAdmins } + return { + userPermissions: prependOwnerRow(userPermissions, ownerUsername), + shareAdmins: prependOwnerRow(shareAdmins, ownerUsername), + } +} + +function buildRecordFields(record: DRecord, unmask: boolean): NsfRecordView['fields'] { + return getRecordFields(record).flatMap((field) => { + if (TOP_LEVEL_FIELD_TYPES.has(field.type)) return [] + const rawValue = Array.isArray(field.value) ? field.value[0] : field.value + if (rawValue == null || rawValue === '') return [] + const value = !unmask && isSensitiveFieldType(field.type) ? MASKED_VALUE : String(rawValue) + return [{ type: field.type, label: field.label, value }] + }) +} + +function formatRecordUserPermissionBlock(entry: NsfRecordPermission): string[] { + const lines: string[] = [] + if (entry.username) lines.push(` User: ${entry.username}`) + else if (entry.accountUid) lines.push(` User UID: ${entry.accountUid}`) + if (entry.owner) lines.push(' Owner: Yes') + lines.push(` Shareable: ${entry.shareable ? 'Yes' : 'No'}`) + lines.push(` Read-Only: ${entry.editable ? 'No' : 'Yes'}`) + return lines +} + +async function fetchRecordPermissions(auth: Auth, recordUid: string): Promise { + try { + const response = await auth.executeRest( + recordDetailsMessage(recordUid, Records.RecordDetailsInclude.SHARE_ONLY) + ) + const detail = response.recordDataWithAccessInfo?.[0] + return (detail?.userPermission ?? []).map((entry) => ({ + username: entry.username || '', + accountUid: bytesToUid(entry.accountUid), + owner: !!entry.owner, + shareAdmin: !!entry.shareAdmin, + shareable: !!entry.sharable, + editable: !!entry.editable, + awaitingApproval: !!entry.awaitingApproval, + expiration: longToNumber(entry.expiration as number | null | undefined), + })) + } catch (err) { + throw new KeeperSdkError( + `Failed to fetch record permissions for ${recordUid}: ${extractErrorMessage(err)}` + ) + } +} + +async function fetchRecordShareAdmins(auth: Auth, recordUid: string): Promise { + try { + const response = await auth.executeRest(recordSharingAdminsMessage(recordUid)) + return (response.userProfileExts ?? []) + .map((ext) => ext.email || '') + .filter((email) => email.length > 0) + .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + } catch { + return [] + } +} + +async function fetchRecordDataFallback(auth: Auth, recordUid: string): Promise { + try { + const response = await auth.executeRest( + recordDetailsMessage(recordUid, Records.RecordDetailsInclude.DATA_ONLY) + ) + return response.recordDataWithAccessInfo?.[0]?.recordData as DRecord['data'] | undefined + } catch { + return undefined + } +} + +function buildFolderView(storage: InMemoryStorage, folderUid: string): NsfFolderView { + const folder = getKeeperDriveFolder(storage, folderUid) + if (!folder) { + throw new KeeperSdkError(`Nested share folder not found: ${folderUid}`, ResultCodes.NSF_NOT_FOUND) + } + + const split = splitFolderPermissions(storage, folder) + const { userPermissions, shareAdmins } = ensureFolderOwnerListed( + folder, + split.userPermissions, + split.shareAdmins + ) + + return { + objectType: 'folder', + folderUid, + name: folder.data.name || 'Unnamed', + parentUid: normalizeParentUid(folder.parentUid), + path: buildFolderPath(storage, folderUid), + userPermissions, + shareAdmins, + teamPermissions: split.teamPermissions, + records: collectRecordsInFolder(storage, folderUid).map((record) => ({ + uid: record.uid, + title: getRecordTitle(record), + type: getRecordType(record), + })), + } +} + +async function buildRecordView( + auth: Auth, + storage: InMemoryStorage, + recordUid: string, + unmask: boolean +): Promise { + let record = getKeeperDriveRecord(storage, recordUid) + if (!record) { + throw new KeeperSdkError(`Nested share record not found: ${recordUid}`, ResultCodes.NSF_NOT_FOUND) + } + + let title = getRecordTitle(record) + if (UNKNOWN_RECORD_TITLES.has(title)) { + const fallbackData = await fetchRecordDataFallback(auth, recordUid) + if (fallbackData) { + record = { ...record, data: fallbackData } + title = getRecordTitle(record) + } + } + + const password = getRecordPassword(record) + const [userPermissions, shareAdmins] = await Promise.all([ + fetchRecordPermissions(auth, recordUid), + fetchRecordShareAdmins(auth, recordUid), + ]) + const notes = + typeof record.data?.notes === 'string' && record.data.notes.trim() ? record.data.notes.trim() : undefined + + return { + objectType: 'record', + recordUid, + title, + type: getRecordType(record), + revision: record.revision, + version: record.version, + folderLocation: findRecordFolderLocation(storage, recordUid) || 'root', + login: getRecordLogin(record) || undefined, + password: password ? (unmask ? password : MASKED_VALUE) : undefined, + url: getRecordUrl(record) || undefined, + notes, + fields: buildRecordFields(record, unmask), + userPermissions, + shareAdmins, + } +} + +export function resolveNsfFolder(storage: InMemoryStorage, identifier: string): string | undefined { + return resolveByUidOrName( + getKeeperDriveFolders(storage), + identifier, + (folder) => folder.uid, + (folder) => folder.data.name || '' + )?.uid +} + +export function resolveNsfRecord(storage: InMemoryStorage, identifier: string): string | undefined { + const trimmed = identifier.trim() + if (!trimmed) return undefined + return getKeeperDriveRecord(storage, trimmed)?.uid ?? resolveRecordByTitleSearch(storage, trimmed)?.uid +} + +export async function getNestedShareFolder( + storage: InMemoryStorage, + auth: Auth, + identifier: string, + options: GetNsfOptions = {} +): Promise { + const trimmed = identifier.trim() + if (!trimmed) { + throw new KeeperSdkError('UID or title is required.', ResultCodes.NSF_NOT_FOUND) + } + + const folderUid = resolveNsfFolder(storage, trimmed) + if (folderUid) { + return { kind: 'folder', view: buildFolderView(storage, folderUid) } + } + + const recordUid = resolveNsfRecord(storage, trimmed) + if (recordUid) { + const view = await buildRecordView(auth, storage, recordUid, options.unmask ?? false) + return { kind: 'record', view } + } + + throw new KeeperSdkError( + `Cannot find any Nested Share Folder object with UID or title: ${trimmed}`, + ResultCodes.NSF_NOT_FOUND + ) +} + +export function formatNsfFolderDetail(view: NsfFolderView, verbose = false): string { + const lines = [ + folderDetailRow('Nested Share Folder UID', view.folderUid), + folderDetailRow('Name', view.name), + '', + FOLDER_USER_PERMISSIONS_HEADING, + ...view.userPermissions.map((entry) => `${entry.username}: ${entry.role}`), + '', + FOLDER_SHARE_ADMINS_HEADING, + ...view.shareAdmins.map((entry) => `${entry.username}: ${entry.role}`), + ] + + if (!verbose) return lines.join('\n') + + lines.push('', folderDetailRow('Parent UID', view.parentUid), folderDetailRow('Path', view.path)) + if (view.records.length > 0) { + lines.push('', 'Records:') + for (const record of view.records) { + lines.push(` ${record.uid} ${record.title} (${record.type})`) + } + } + if (view.teamPermissions.length > 0) { + lines.push('', 'Team Permissions:') + for (const entry of view.teamPermissions) { + lines.push(` ${entry.accessTypeUid} role=${entry.accessRoleType}`) + } + } + return lines.join('\n') +} + +export function formatNsfRecordDetail(view: NsfRecordView, verbose = false): string { + const lines = [ + recordDetailRow('UID', view.recordUid), + recordDetailRow('Type', view.type), + recordDetailRow('Title', view.title), + ] + + if (view.login) lines.push(recordDetailRow('Login', view.login)) + if (view.password) lines.push(recordDetailRow('Password', view.password)) + if (view.url) lines.push(recordDetailRow('Url', view.url)) + if (view.notes) lines.push(recordDetailRow('Notes', view.notes)) + + for (const field of view.fields) { + const label = field.label || field.type + lines.push(recordDetailRow(label.charAt(0).toUpperCase() + label.slice(1), field.value)) + } + + if (view.userPermissions.length > 0) { + lines.push('', RECORD_USER_PERMISSIONS_HEADING) + for (const entry of view.userPermissions) { + lines.push('', ...formatRecordUserPermissionBlock(entry)) + } + } + + if (view.shareAdmins.length > 0) { + lines.push('', `Share Admins (${view.shareAdmins.length}):`) + for (const admin of view.shareAdmins) { + lines.push(` ${admin}`) + } + } + + if (verbose) { + lines.push( + '', + recordDetailRow('Folder', view.folderLocation), + recordDetailRow('Revision', String(view.revision)), + recordDetailRow('Version', String(view.version)) + ) + } + + return lines.join('\n') +} + +export function formatNsfDetail(result: GetNsfResult, verbose = false): string { + return result.kind === 'folder' + ? formatNsfFolderDetail(result.view, verbose) + : formatNsfRecordDetail(result.view, verbose) +} diff --git a/KeeperSdk/src/nestedShareFolders/index.ts b/KeeperSdk/src/nestedShareFolders/index.ts new file mode 100644 index 0000000..d03d09f --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/index.ts @@ -0,0 +1,54 @@ +export { + ROOT_FOLDER_UID, + KeeperDriveKind, + NsfItemType, + formatAccessRoleType, + formatAccessType, + normalizeParentUid, + isRootFolderUid, + getKeeperDriveFolders, + getKeeperDriveRecords, + findRecordFolderLocation, + buildFolderPath, + isSensitiveFieldType, + resolveAccessUsername, + folderAccessDisplayRole, +} from './nsfHelpers' + +export { + ListNsfFormat, + listNestedShareFolders, + formatListNsfTable, + renderListNsfAsciiTable, + formatListNsfCsv, + formatListNsfJson, + formatListNsfOutput, +} from './listNsf' +export type { + ListNsfFormatInput, + ListNsfOptions, + ListNsfRow, + FormattedListNsfTable, +} from './listNsf' + +export { + GetNsfFormat, + resolveNsfFolder, + resolveNsfRecord, + getNestedShareFolder, + formatNsfFolderDetail, + formatNsfRecordDetail, + formatNsfDetail, +} from './getNsf' +export type { + GetNsfFormatInput, + GetNsfOptions, + GetNsfResult, + NsfFolderView, + NsfRecordView, + NsfFolderPermission, + NsfFolderAccessRow, + NsfRecordPermission, +} from './getNsf' + +export { NestedShareFolderManager } from './NestedShareFolderManager' diff --git a/KeeperSdk/src/nestedShareFolders/listNsf.ts b/KeeperSdk/src/nestedShareFolders/listNsf.ts new file mode 100644 index 0000000..ef7da4d --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/listNsf.ts @@ -0,0 +1,172 @@ +import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { + NsfItemType, + findRecordFolderLocation, + getKeeperDriveFolders, + getKeeperDriveRecords, + getRecordDescription, + normalizeParentUid, +} from './nsfHelpers' +import { getRecordTitle, getRecordType } from '../records/RecordUtils' + +export enum ListNsfFormat { + Table = 'table', + CSV = 'csv', + JSON = 'json', +} + +export type ListNsfFormatInput = ListNsfFormat | `${ListNsfFormat}` + +export type ListNsfOptions = { + folders?: boolean + records?: boolean + format?: ListNsfFormatInput +} + +export type ListNsfRow = { + itemType: NsfItemType + uid: string + title: string + type: string + description: string + parentOrFolder: string +} + +export type FormattedListNsfTable = { + headers: string[] + rows: string[][] +} + +const TABLE_HEADERS = ['#', 'Item Type', 'UID', 'Title', 'Type', 'Description'] as const +const FULL_HEADERS = ['Item Type', 'UID', 'Title', 'Type', 'Description', 'Parent/Folder'] as const +const DEFAULT_COLUMN_WIDTH = 40 +const MIN_TRUNCATE_PREFIX = 3 + +function compareRows(a: ListNsfRow, b: ListNsfRow): number { + const typeCompare = a.itemType.localeCompare(b.itemType) + return typeCompare !== 0 ? typeCompare : a.title.localeCompare(b.title, undefined, { sensitivity: 'base' }) +} + +function collectFolderRows(storage: InMemoryStorage): ListNsfRow[] { + return getKeeperDriveFolders(storage).map((folder) => ({ + itemType: NsfItemType.Folder, + uid: folder.uid, + title: folder.data.name || 'Unnamed', + type: '', + description: '', + parentOrFolder: normalizeParentUid(folder.parentUid), + })) +} + +function collectRecordRows(storage: InMemoryStorage): ListNsfRow[] { + return getKeeperDriveRecords(storage).map((record) => ({ + itemType: NsfItemType.Record, + uid: record.uid, + title: getRecordTitle(record), + type: getRecordType(record), + description: getRecordDescription(record), + parentOrFolder: findRecordFolderLocation(storage, record.uid) || 'root', + })) +} + +export function listNestedShareFolders(storage: InMemoryStorage, options: ListNsfOptions = {}): ListNsfRow[] { + const showFolders = options.folders ?? options.records == null + const showRecords = options.records ?? options.folders == null + const rows: ListNsfRow[] = [] + if (showFolders) rows.push(...collectFolderRows(storage)) + if (showRecords) rows.push(...collectRecordRows(storage)) + return rows.sort(compareRows) +} + +function truncateText(text: string, maxLength: number): string { + if (!text || text.length <= maxLength) return text + if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength) + return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...` +} + +export function formatListNsfTable( + rows: ListNsfRow[], + options: { columnWidth?: number } = {} +): FormattedListNsfTable { + const columnWidth = options.columnWidth ?? DEFAULT_COLUMN_WIDTH + const outRows = rows.map((row, index) => [ + String(index + 1), + row.itemType, + truncateText(row.uid, columnWidth), + truncateText(row.title, columnWidth), + truncateText(row.type, columnWidth), + truncateText(row.description, columnWidth), + ]) + return { headers: [...TABLE_HEADERS], rows: outRows } +} + +export function renderListNsfAsciiTable( + table: FormattedListNsfTable, + options: { minColWidth?: number } = {} +): string { + const { minColWidth = 2 } = options + const { headers, rows } = table + const columnCount = headers.length + const columnWidths = headers.map((header, columnIndex) => { + let width = Math.max(header.length, minColWidth) + for (const row of rows) { + width = Math.max(width, (row[columnIndex] || '').length, minColWidth) + } + return width + }) + const padCell = (cell: string, columnIndex: number) => + cell + ' '.repeat(columnWidths[columnIndex] - cell.length) + const formatRow = (cells: string[]) => cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ') + const ruleRow = columnWidths.map((width, columnIndex) => padCell('-'.repeat(width), columnIndex)).join(' ') + return [formatRow(headers), ruleRow, ...rows.map(formatRow)].join('\n') +} + +function escapeCsvCell(value: string): string { + if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"` + return value +} + +export function formatListNsfCsv(rows: ListNsfRow[]): string { + const lines = [FULL_HEADERS.join(',')] + for (const row of rows) { + lines.push( + [ + row.itemType, + row.uid, + row.title, + row.type, + row.description, + row.parentOrFolder, + ] + .map(escapeCsvCell) + .join(',') + ) + } + return lines.join('\n') +} + +export function formatListNsfJson(rows: ListNsfRow[]): string { + return JSON.stringify( + rows.map((row) => ({ + item_type: row.itemType, + uid: row.uid, + title: row.title, + type: row.type, + description: row.description, + parent_or_folder: row.parentOrFolder, + })), + null, + 2 + ) +} + +export function formatListNsfOutput(rows: ListNsfRow[], format: ListNsfFormatInput = ListNsfFormat.Table): string { + switch (format) { + case ListNsfFormat.CSV: + return formatListNsfCsv(rows) + case ListNsfFormat.JSON: + return formatListNsfJson(rows) + default: + return renderListNsfAsciiTable(formatListNsfTable(rows)) + } +} diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts new file mode 100644 index 0000000..a071c71 --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -0,0 +1,194 @@ +import type { + DRecord, + DUser, + DKdFolder, + DKdFolderAccess, + Dependency, +} from '@keeper-security/keeperapi' +import { Folder, webSafe64FromBytes } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../storage/InMemoryStorage' + +const ACCESS_ROLE_LABELS: Record = { + [Folder.AccessRoleType.NAVIGATOR]: 'navigator', + [Folder.AccessRoleType.REQUESTOR]: 'requestor', + [Folder.AccessRoleType.VIEWER]: 'viewer', + [Folder.AccessRoleType.SHARED_MANAGER]: 'shared-manager', + [Folder.AccessRoleType.CONTENT_MANAGER]: 'content-manager', + [Folder.AccessRoleType.CONTENT_SHARE_MANAGER]: 'content-share-manager', + [Folder.AccessRoleType.MANAGER]: 'manager', + [Folder.AccessRoleType.UNRESOLVED]: 'unresolved', +} + +const ACCESS_TYPE_LABELS: Record = { + [Folder.AccessType.AT_USER]: 'user', + [Folder.AccessType.AT_TEAM]: 'team', + [Folder.AccessType.AT_OWNER]: 'owner', + [Folder.AccessType.AT_ENTERPRISE]: 'enterprise', + [Folder.AccessType.AT_FOLDER]: 'folder', + [Folder.AccessType.AT_APPLICATION]: 'application', +} + +const SENSITIVE_FIELD_TYPES = new Set(['password', 'secret', 'pinCode']) +const NOTE_FIELD_TYPES = new Set(['note', 'multiline']) +const RECORD_DESCRIPTION_MAX_LENGTH = 120 + +export const ROOT_FOLDER_UID = 'AAAAAAAAAAAAAAAAAPmtNA' + +export enum KeeperDriveKind { + Folder = 'keeper_drive_folder', + FolderAccess = 'keeper_drive_folder_access', + FolderRecord = 'keeper_drive_folder_record', + RecordAccess = 'keeper_drive_record_access', +} + +export enum NsfItemType { + Folder = 'Folder', + Record = 'Record', +} + +export function formatAccessRoleType(role: Folder.AccessRoleType | null | undefined): string { + if (role == null) return 'unknown' + return ACCESS_ROLE_LABELS[role] ?? `role-${role}` +} + +export function formatAccessType(type: Folder.AccessType | null | undefined): string { + if (type == null) return 'unknown' + return ACCESS_TYPE_LABELS[type] ?? `type-${type}` +} + +export function normalizeParentUid(parentUid: string | undefined | null): string { + const value = (parentUid ?? '').trim() + return !value || value === ROOT_FOLDER_UID ? ROOT_FOLDER_UID : value +} + +export function isRootFolderUid(folderUid: string | undefined | null): boolean { + return normalizeParentUid(folderUid) === ROOT_FOLDER_UID +} + +export function getKeeperDriveFolders(storage: InMemoryStorage): DKdFolder[] { + return storage.getAll(KeeperDriveKind.Folder) +} + +export function getKeeperDriveRecords(storage: InMemoryStorage): DRecord[] { + return storage.getRecords().filter((record) => record.isKeeperDriveData) +} + +export function getKeeperDriveFolder(storage: InMemoryStorage, folderUid: string): DKdFolder | undefined { + return storage.getByUid(KeeperDriveKind.Folder, folderUid) +} + +export function getKeeperDriveRecord(storage: InMemoryStorage, recordUid: string): DRecord | undefined { + const record = storage.getByUid('record', recordUid) + return record?.isKeeperDriveData ? record : undefined +} + +export function getFolderAccessEntries(storage: InMemoryStorage, folderUid: string): DKdFolderAccess[] { + return storage + .getAll(KeeperDriveKind.FolderAccess) + .filter((entry) => entry.folderUid === folderUid) +} + +export function getFolderDisplayName(storage: InMemoryStorage, folderUid: string): string { + if (isRootFolderUid(folderUid)) return 'root' + return getKeeperDriveFolder(storage, folderUid)?.data.name ?? folderUid +} + +export function findRecordFolderLocation(storage: InMemoryStorage, recordUid: string): string { + for (const folder of getKeeperDriveFolders(storage)) { + const children = storage.getDependenciesSync(folder.uid) + if (children?.some((child: Dependency) => child.uid === recordUid && child.kind === 'record')) { + return isRootFolderUid(folder.uid) ? 'root' : folder.data.name || folder.uid + } + } + return 'root' +} + +export function buildFolderPath(storage: InMemoryStorage, folderUid: string): string { + if (isRootFolderUid(folderUid)) return '/' + + const segments: string[] = [] + let currentUid: string | undefined = folderUid + const seen = new Set() + + while (currentUid && !isRootFolderUid(currentUid) && !seen.has(currentUid)) { + seen.add(currentUid) + const folder = getKeeperDriveFolder(storage, currentUid) + if (!folder) break + segments.unshift(folder.data.name || folder.uid) + currentUid = folder.parentUid + } + + return `/${segments.join('/')}` +} + +export function collectRecordsInFolder(storage: InMemoryStorage, folderUid: string): DRecord[] { + const children = storage.getDependenciesSync(folderUid) + if (!children?.length) return [] + + const records: DRecord[] = [] + for (const child of children) { + if (child.kind !== 'record') continue + const record = getKeeperDriveRecord(storage, child.uid) + if (record) records.push(record) + } + return records +} + +export function getRecordDescription(record: DRecord): string { + const data = record.data + if (!data || typeof data !== 'object') return '' + + const fields = Array.isArray(data.fields) ? data.fields : [] + for (const field of fields) { + if (!NOTE_FIELD_TYPES.has(field?.type)) continue + const value = Array.isArray(field.value) ? field.value[0] : field.value + if (typeof value === 'string' && value.trim()) { + return value.trim().slice(0, RECORD_DESCRIPTION_MAX_LENGTH) + } + } + + if (typeof data.notes === 'string' && data.notes.trim()) { + return data.notes.trim().slice(0, RECORD_DESCRIPTION_MAX_LENGTH) + } + return '' +} + +export function isSensitiveFieldType(fieldType: string): boolean { + return SENSITIVE_FIELD_TYPES.has(fieldType) +} + +export function resolveAccessUsername( + storage: InMemoryStorage, + accessTypeUid: string, + folder?: DKdFolder +): string { + for (const user of storage.getAll('user')) { + if (webSafe64FromBytes(user.accountUid) === accessTypeUid) { + return user.username + } + } + if (folder?.ownerInfo?.accountUid === accessTypeUid && folder.ownerInfo.username) { + return folder.ownerInfo.username + } + return accessTypeUid +} + +export function folderAccessDisplayRole(entry: DKdFolderAccess): string { + if (entry.accessType === Folder.AccessType.AT_OWNER) return 'owner' + return formatAccessRoleType(entry.accessRoleType) +} + +export function isFolderShareAdministrator(entry: DKdFolderAccess): boolean { + return ( + entry.accessType === Folder.AccessType.AT_OWNER || + entry.accessRoleType === Folder.AccessRoleType.MANAGER || + entry.accessRoleType === Folder.AccessRoleType.CONTENT_SHARE_MANAGER + ) +} + +export function isFolderUserPermission(entry: DKdFolderAccess): boolean { + return ( + entry.accessType === Folder.AccessType.AT_USER || + entry.accessType === Folder.AccessType.AT_OWNER + ) +} diff --git a/KeeperSdk/src/sharing/Sharing.ts b/KeeperSdk/src/sharing/Sharing.ts index 9046f36..4dcc207 100644 --- a/KeeperSdk/src/sharing/Sharing.ts +++ b/KeeperSdk/src/sharing/Sharing.ts @@ -4,7 +4,7 @@ import { Authentication, platform, getPublicKeysMessage, - getRecordsDetailsMessage, + keeperDriveRecordAccessByUid, webSafe64FromBytes, recordsShareUpdateMessage, normal64Bytes, @@ -241,15 +241,9 @@ function longToNumber(value: number | { toNumber: () => number } | null | undefi } export async function getRecordShareInfo(auth: Auth, recordUid: string): Promise { - const msg = getRecordsDetailsMessage({ - clientTime: Date.now(), - recordUid: [normal64Bytes(recordUid)], - recordDetailsInclude: Records.RecordDetailsInclude.SHARE_ONLY, - }) - - let response: Records.IGetRecordDataWithAccessInfoResponse + let response try { - response = await auth.executeRest(msg) + response = await auth.executeRest(keeperDriveRecordAccessByUid(recordUid)) } catch (err) { throw new KeeperSdkError( `Failed to fetch share info for ${recordUid}: ${extractErrorMessage(err)}` diff --git a/KeeperSdk/src/storage/InMemoryStorage.ts b/KeeperSdk/src/storage/InMemoryStorage.ts index 89424b9..dc1ba49 100644 --- a/KeeperSdk/src/storage/InMemoryStorage.ts +++ b/KeeperSdk/src/storage/InMemoryStorage.ts @@ -74,6 +74,10 @@ export class InMemoryStorage implements VaultStorage { return this.dependenciesByParent.get(uid) } + public getDependenciesSync(uid: string): Dependency[] | undefined { + return this.dependenciesByParent.get(uid) + } + public async addDependencies(dependencies: Dependencies): Promise { for (const [parentUid, children] of Object.entries(dependencies)) { if (!this.dependenciesByParent.has(parentUid)) { @@ -137,6 +141,7 @@ export class InMemoryStorage implements VaultStorage { token?: string sharedFolderUid?: string recordUid?: string + folderUid?: string accountUid?: string | Uint8Array teamUid?: string } @@ -157,6 +162,9 @@ export class InMemoryStorage implements VaultStorage { if (record.sharedFolderUid && record.teamUid) { return `${record.sharedFolderUid}:${record.teamUid}` } + if (record.folderUid && record.recordUid) { + return `${record.folderUid}:${record.recordUid}` + } if (item.kind === VaultObjectKind.User && accountUidStr) return accountUidStr return '_singleton_' } diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index b33a1b8..2aca283 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -54,6 +54,11 @@ export enum RoleErrorCode { RoleEnforcementFailed = 'role_enforcement_failed', } +export enum NsfErrorCode { + NotFound = 'nsf_not_found', + MultipleMatches = 'nsf_multiple_matches', +} + export enum TeamErrorCode { TeamRequired = 'team_required', TeamNotFound = 'team_not_found', @@ -121,6 +126,8 @@ export const ResultCodes = { ROLE_RENAME_MULTI_NOT_ALLOWED: RoleErrorCode.RoleRenameMultiNotAllowed, ROLE_NAME_EMPTY: RoleErrorCode.RoleNameEmpty, ROLE_ENFORCEMENT_FAILED: RoleErrorCode.RoleEnforcementFailed, + NSF_NOT_FOUND: NsfErrorCode.NotFound, + MULTIPLE_NSF_MATCHES: NsfErrorCode.MultipleMatches, TEAM_REQUIRED: TeamErrorCode.TeamRequired, TEAM_NOT_FOUND: TeamErrorCode.TeamNotFound, MULTIPLE_TEAM_MATCHES: TeamErrorCode.MultipleTeamMatches, diff --git a/KeeperSdk/src/utils/index.ts b/KeeperSdk/src/utils/index.ts index a7a638f..cef46a3 100644 --- a/KeeperSdk/src/utils/index.ts +++ b/KeeperSdk/src/utils/index.ts @@ -8,6 +8,7 @@ export { RoleErrorCode, TeamErrorCode, UserErrorCode, + NsfErrorCode, KEEPER_PUBLIC_HOSTS, } from './constants' export { Logger, ConsoleLogger, LogLevel, logger, setLogger, getLogger, resetLogger } from './Logger' diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 52c3055..aa1976c 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -75,6 +75,9 @@ import { type UpdateRoleResult, } from '../roles' import { UserManager } from '../users/UserManager' +import { NestedShareFolderManager } from '../nestedShareFolders/NestedShareFolderManager' +import type { ListNsfOptions, ListNsfRow, ListNsfFormatInput, FormattedListNsfTable } from '../nestedShareFolders/listNsf' +import type { GetNsfOptions, GetNsfResult } from '../nestedShareFolders/getNsf' import type { ListUserRow, ListUsersOptions, @@ -142,6 +145,7 @@ export class KeeperVault { private readonly teamManager: TeamManager private readonly roleManager: RoleManager private readonly userManager: UserManager + private readonly nestedShareFolderManager: NestedShareFolderManager constructor(config?: KeeperVaultConfig) { this.config = { @@ -165,6 +169,11 @@ export class KeeperVault { this.teamManager = new TeamManager(authProvider) this.roleManager = new RoleManager(authProvider) this.userManager = new UserManager(authProvider) + this.nestedShareFolderManager = new NestedShareFolderManager(this.storage, authProvider) + } + + public getNestedShareFolderManager(): NestedShareFolderManager { + return this.nestedShareFolderManager } public getFolderManager(): FolderManager { @@ -696,6 +705,31 @@ export class KeeperVault { return getRecordShareInfoOp(auth, recordUid) } + public listNestedShareFolders(options?: ListNsfOptions): ListNsfRow[] { + this.getAuthOrThrow() + return this.nestedShareFolderManager.listNestedShareFolders(options ?? {}) + } + + public formatListNsfTable(rows: ListNsfRow[], options?: { columnWidth?: number }): FormattedListNsfTable { + return this.nestedShareFolderManager.formatListNsfTable(rows, options ?? {}) + } + + public renderListNsfAsciiTable(table: FormattedListNsfTable, options?: { minColWidth?: number }): string { + return this.nestedShareFolderManager.renderListNsfAsciiTable(table, options ?? {}) + } + + public formatListNsfOutput(rows: ListNsfRow[], format?: ListNsfFormatInput): string { + return this.nestedShareFolderManager.formatListNsfOutput(rows, format) + } + + public async getNestedShareFolder(identifier: string, options?: GetNsfOptions): Promise { + return this.nestedShareFolderManager.getNestedShareFolder(identifier, options ?? {}) + } + + public formatNsfDetail(result: GetNsfResult, verbose?: boolean): string { + return this.nestedShareFolderManager.formatNsfDetail(result, verbose ?? false) + } + 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 f53fc68..268c9f5 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -29,6 +29,8 @@ "roles:add": "ts-node src/roles/addRole.ts", "roles:update": "ts-node src/roles/updateRole.ts", "roles:delete": "ts-node src/roles/deleteRole.ts", + "nsf:list": "ts-node src/nestedShareFolders/list_nsf.ts", + "nsf:get": "ts-node src/nestedShareFolders/get_nsf.ts", "teams:list": "ts-node src/teams/list_teams.ts", "teams:view": "ts-node src/teams/view_team.ts", "teams:add": "ts-node src/teams/add_team.ts", diff --git a/examples/sdk_example/src/nestedShareFolders/get_nsf.ts b/examples/sdk_example/src/nestedShareFolders/get_nsf.ts new file mode 100644 index 0000000..33a4c7b --- /dev/null +++ b/examples/sdk_example/src/nestedShareFolders/get_nsf.ts @@ -0,0 +1,54 @@ +import { + cleanup, + extractErrorMessage, + GetNsfFormat, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +async function getNsf() { + const vault = await login() + + try { + const identifier = (await prompt('Record UID, folder UID, or title: ')).trim() + if (!identifier) { + logger.info('No UID or title given.') + return + } + + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + const verbose = isYes(await prompt('Verbose permissions? [y/N]: ')) + const unmask = isYes(await prompt('Unmask secrets? [y/N]: ')) + + const restore = suppressLogs() + let result + try { + result = await vault.getNestedShareFolder(identifier, { + format: asJson ? GetNsfFormat.JSON : GetNsfFormat.Detail, + verbose, + unmask, + }) + } finally { + restore() + } + + logger.info('') + if (asJson) { + logger.info(JSON.stringify(result.view, null, 2)) + } else { + logger.info(vault.formatNsfDetail(result, verbose)) + } + logger.info('') + } catch (err) { + logger.error(`Lookup failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(getNsf) diff --git a/examples/sdk_example/src/nestedShareFolders/list_nsf.ts b/examples/sdk_example/src/nestedShareFolders/list_nsf.ts new file mode 100644 index 0000000..20896bb --- /dev/null +++ b/examples/sdk_example/src/nestedShareFolders/list_nsf.ts @@ -0,0 +1,69 @@ +import fs from 'fs/promises' +import { + cleanup, + extractErrorMessage, + ListNsfFormat, + login, + logger, + prompt, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +type ListMode = 'all' | 'folders' | 'records' + +const MODE_BY_INPUT: Record = { + '': 'all', + '1': 'all', + '2': 'folders', + '3': 'records', + all: 'all', + folders: 'folders', + records: 'records', +} + +function parseMode(input: string): ListMode { + return MODE_BY_INPUT[input.trim().toLowerCase()] ?? 'all' +} + +async function listNsf() { + const vault = await login() + + try { + logger.info('Show: 1) all 2) folders only 3) records only') + const mode = parseMode(await prompt('Choose [1]: ')) + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + const asCsv = !asJson && isYes(await prompt('Output as CSV? [y/N]: ')) + const outputPath = (await prompt('Output file path (Enter for stdout): ')).trim() + + const format = asJson ? ListNsfFormat.JSON : asCsv ? ListNsfFormat.CSV : ListNsfFormat.Table + const rows = vault.listNestedShareFolders({ + folders: mode !== 'records', + records: mode !== 'folders', + }) + + if (rows.length === 0) { + logger.info('No nested share folder items found.') + return + } + + const output = vault.formatListNsfOutput(rows, format) + if (outputPath && format !== ListNsfFormat.Table) { + await fs.writeFile(outputPath, output, 'utf-8') + logger.info(`Wrote ${rows.length} row(s) to ${outputPath}`) + return + } + + logger.info('') + logger.info(output) + logger.info('') + logger.info(`Total: ${rows.length} item${rows.length === 1 ? '' : 's'}`) + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listNsf) diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index 6b32762..a348e40 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -451,6 +451,36 @@ export const getRecordsDetailsMessage = ( Records.GetRecordDataWithAccessInfoResponse ) +export const keeperDriveRecordAccessMessage = ( + recordUids: Uint8Array[], + clientTime: number = Date.now() +): RestMessage => + getRecordsDetailsMessage({ + clientTime, + recordUid: recordUids, + recordDetailsInclude: Records.RecordDetailsInclude.SHARE_ONLY, + }) + +export const keeperDriveRecordDataMessage = ( + recordUids: Uint8Array[], + clientTime: number = Date.now() +): RestMessage => + getRecordsDetailsMessage({ + clientTime, + recordUid: recordUids, + recordDetailsInclude: Records.RecordDetailsInclude.DATA_ONLY, + }) + +export const getSharingAdminsMessage = ( + data: Enterprise.IGetSharingAdminsRequest +): RestMessage => + createMessage( + data, + 'enterprise/get_sharing_admins', + Enterprise.GetSharingAdminsRequest, + Enterprise.GetSharingAdminsResponse + ) + export const recordsAddMessage = ( data: Records.IRecordsAddRequest ): RestMessage => From 11e067339df60324e6f388b40765c2df7b4297c7 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Fri, 12 Jun 2026 13:56:25 +0530 Subject: [PATCH 14/14] added nsf ln and nsf rm --- KeeperSdk/src/auth/ConsoleLogin.ts | 2 + KeeperSdk/src/index.ts | 9 + .../NestedShareFolderManager.ts | 22 ++ KeeperSdk/src/nestedShareFolders/getNsf.ts | 55 +--- KeeperSdk/src/nestedShareFolders/index.ts | 23 ++ .../src/nestedShareFolders/linkNsfRecord.ts | 148 ++++++++++ .../src/nestedShareFolders/nsfHelpers.ts | 157 +++++++++++ .../nestedShareFolders/nsfRemoveMessages.ts | 257 ++++++++++++++++++ .../src/nestedShareFolders/removeNsfRecord.ts | 247 +++++++++++++++++ KeeperSdk/src/sharing/Sharing.ts | 4 +- KeeperSdk/src/utils/constants.ts | 16 ++ KeeperSdk/src/vault/KeeperVault.ts | 31 +++ examples/sdk_example/package.json | 2 + .../src/nestedShareFolders/link_nsf.ts | 44 +++ .../src/nestedShareFolders/remove_nsf.ts | 119 ++++++++ keeperapi/src/restMessages.ts | 10 + 16 files changed, 1095 insertions(+), 51 deletions(-) create mode 100644 KeeperSdk/src/nestedShareFolders/linkNsfRecord.ts create mode 100644 KeeperSdk/src/nestedShareFolders/nsfRemoveMessages.ts create mode 100644 KeeperSdk/src/nestedShareFolders/removeNsfRecord.ts create mode 100644 examples/sdk_example/src/nestedShareFolders/link_nsf.ts create mode 100644 examples/sdk_example/src/nestedShareFolders/remove_nsf.ts diff --git a/KeeperSdk/src/auth/ConsoleLogin.ts b/KeeperSdk/src/auth/ConsoleLogin.ts index 3d0907e..92863b5 100644 --- a/KeeperSdk/src/auth/ConsoleLogin.ts +++ b/KeeperSdk/src/auth/ConsoleLogin.ts @@ -342,6 +342,8 @@ async function syncVault(vault: KeeperVault): Promise { logger.info('Syncing vault...') await withSuppressedOutput(() => vault.sync()) logger.info(`Vault synced. ${vault.getSummary().recordCount} records loaded.\n`) + // CLI examples use REST only; close push websocket to avoid async key errors during prompts. + vault.releasePushConnection() return vault } diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index ca5c06c..317c7da 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -471,6 +471,10 @@ export { formatNsfFolderDetail, formatNsfRecordDetail, formatNsfDetail, + linkNestedShareRecord, + NsfRemoveOperation, + removeNestedShareRecords, + formatRemoveNsfPreview, NestedShareFolderManager, } from './nestedShareFolders' export type { @@ -486,6 +490,11 @@ export type { NsfFolderPermission, NsfFolderAccessRow, NsfRecordPermission, + LinkNsfRecordResult, + NsfRemoveOperationInput, + RemoveNsfRecordInput, + NsfRemovePreviewItem, + RemoveNsfRecordResult, } from './nestedShareFolders' export type { diff --git a/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts b/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts index bf1169b..4a21c74 100644 --- a/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts +++ b/KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts @@ -21,6 +21,13 @@ import { type NsfFolderView, type NsfRecordView, } from './getNsf' +import { linkNestedShareRecord, type LinkNsfRecordResult } from './linkNsfRecord' +import { + formatRemoveNsfPreview, + removeNestedShareRecords, + type RemoveNsfRecordInput, + type RemoveNsfRecordResult, +} from './removeNsfRecord' export type AuthProvider = () => Auth @@ -72,4 +79,19 @@ export class NestedShareFolderManager { public formatNsfRecordDetail(view: NsfRecordView, verbose = false): string { return renderNsfRecordDetail(view, verbose) } + + public async linkNestedShareRecord( + recordIdentifier: string, + folderIdentifier: string + ): Promise { + return linkNestedShareRecord(this.storage, this.requireAuth(), recordIdentifier, folderIdentifier) + } + + public async removeNestedShareRecords(input: RemoveNsfRecordInput): Promise { + return removeNestedShareRecords(this.storage, this.requireAuth(), input) + } + + public formatRemoveNsfPreview(preview: RemoveNsfRecordResult['preview']): string { + return formatRemoveNsfPreview(preview) + } } diff --git a/KeeperSdk/src/nestedShareFolders/getNsf.ts b/KeeperSdk/src/nestedShareFolders/getNsf.ts index cf92712..f1029f9 100644 --- a/KeeperSdk/src/nestedShareFolders/getNsf.ts +++ b/KeeperSdk/src/nestedShareFolders/getNsf.ts @@ -33,6 +33,8 @@ import { isSensitiveFieldType, normalizeParentUid, resolveAccessUsername, + resolveNsfFolderIdentifier, + resolveNsfRecordIdentifier, } from './nsfHelpers' const MASKED_VALUE = '********' @@ -159,46 +161,6 @@ function longToNumber(value: number | { toNumber: () => number } | null | undefi return typeof value === 'number' ? value : value.toNumber() } -function resolveByUidOrName( - items: T[], - identifier: string, - getUid: (item: T) => string, - getName: (item: T) => string -): T | undefined { - const trimmed = identifier.trim() - if (!trimmed) return undefined - - const byUid = items.find((item) => getUid(item) === trimmed) - if (byUid) return byUid - - const lower = trimmed.toLowerCase() - const nameMatches = items.filter((item) => getName(item).toLowerCase() === lower) - if (nameMatches.length === 1) return nameMatches[0] - if (nameMatches.length > 1) { - throw new KeeperSdkError( - `Multiple matches found for "${identifier}". Use a UID instead.`, - ResultCodes.MULTIPLE_NSF_MATCHES - ) - } - return undefined -} - -function resolveRecordByTitleSearch(storage: InMemoryStorage, identifier: string): DRecord | undefined { - const lower = identifier.toLowerCase() - const matches = getKeeperDriveRecords(storage).filter((record) => { - const title = getRecordTitle(record) - return title && lower.length > 0 && title.toLowerCase().includes(lower) - }) - if (matches.length === 1) return matches[0] - if (matches.length > 1) { - throw new KeeperSdkError( - `Multiple records matched "${identifier}". Use a UID instead.`, - ResultCodes.MULTIPLE_NSF_MATCHES - ) - } - return undefined -} - function mapFolderPermission(entry: DKdFolderAccess): NsfFolderPermission { const permission = entry.permission return { @@ -405,18 +367,13 @@ async function buildRecordView( } export function resolveNsfFolder(storage: InMemoryStorage, identifier: string): string | undefined { - return resolveByUidOrName( - getKeeperDriveFolders(storage), - identifier, - (folder) => folder.uid, - (folder) => folder.data.name || '' - )?.uid + return resolveNsfFolderIdentifier(storage, identifier) } export function resolveNsfRecord(storage: InMemoryStorage, identifier: string): string | undefined { - const trimmed = identifier.trim() - if (!trimmed) return undefined - return getKeeperDriveRecord(storage, trimmed)?.uid ?? resolveRecordByTitleSearch(storage, trimmed)?.uid + const uid = resolveNsfRecordIdentifier(storage, identifier) + if (!uid) return undefined + return getKeeperDriveRecord(storage, uid)?.uid } export async function getNestedShareFolder( diff --git a/KeeperSdk/src/nestedShareFolders/index.ts b/KeeperSdk/src/nestedShareFolders/index.ts index d03d09f..d73f651 100644 --- a/KeeperSdk/src/nestedShareFolders/index.ts +++ b/KeeperSdk/src/nestedShareFolders/index.ts @@ -13,6 +13,14 @@ export { isSensitiveFieldType, resolveAccessUsername, folderAccessDisplayRole, + isNestedShareRecord, + isNestedShareFolder, + ensureNestedShareRecord, + ensureNestedShareFolder, + resolveNsfRecordIdentifier, + resolveNsfFolderIdentifier, + findNestedShareFoldersForRecord, + checkRecordDeletePermission, } from './nsfHelpers' export { @@ -51,4 +59,19 @@ export type { NsfRecordPermission, } from './getNsf' +export { linkNestedShareRecord } from './linkNsfRecord' +export type { LinkNsfRecordResult } from './linkNsfRecord' + +export { + NsfRemoveOperation, + removeNestedShareRecords, + formatRemoveNsfPreview, +} from './removeNsfRecord' +export type { + NsfRemoveOperationInput, + RemoveNsfRecordInput, + NsfRemovePreviewItem, + RemoveNsfRecordResult, +} from './removeNsfRecord' + export { NestedShareFolderManager } from './NestedShareFolderManager' diff --git a/KeeperSdk/src/nestedShareFolders/linkNsfRecord.ts b/KeeperSdk/src/nestedShareFolders/linkNsfRecord.ts new file mode 100644 index 0000000..030623a --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/linkNsfRecord.ts @@ -0,0 +1,148 @@ +import type { Auth, DRecord, DRecordMetadata } from '@keeper-security/keeperapi' +import { + Folder, + Records, + folderRecordUpdateMessage, + normal64Bytes, + platform, + webSafe64FromBytes, + type EncryptionType, +} from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { VaultObjectKind } from '../folders/folderHelpers' +import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../utils' +import { + ensureNestedShareFolder, + ensureNestedShareRecord, + resolveNsfFolderIdentifier, + resolveNsfRecordIdentifier, +} from './nsfHelpers' + +export type LinkNsfRecordResult = { + success: boolean + recordUid: string + folderUid: string + status: string + message: string +} + +async function resolveRecordKeyType( + storage: InMemoryStorage, + recordUid: string, + recordVersion: number +): Promise<{ encryptionType: EncryptionType; keyType: Folder.EncryptedKeyType }> { + const metadata = storage.getByUid(VaultObjectKind.Metadata, recordUid) + if (metadata?.recordKeyType === Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY) { + return { + encryptionType: 'cbc', + keyType: Folder.EncryptedKeyType.encrypted_by_data_key, + } + } + if (metadata?.recordKeyType === Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY_GCM || recordVersion >= 3) { + return { + encryptionType: 'gcm', + keyType: Folder.EncryptedKeyType.encrypted_by_data_key_gcm, + } + } + return { + encryptionType: 'gcm', + keyType: Folder.EncryptedKeyType.encrypted_by_data_key_gcm, + } +} + +async function buildRecordMetadata( + storage: InMemoryStorage, + folderUid: string, + recordUid: string +): Promise { + const record = storage.getByUid(VaultObjectKind.Record, recordUid) + const recordKey = await storage.getKeyBytes(recordUid) + const folderKey = await storage.getKeyBytes(folderUid) + if (!recordKey) { + throw new KeeperSdkError( + `Record key not found for ${recordUid}. Run sync() first.`, + ResultCodes.NSF_MISSING_KEY + ) + } + if (!folderKey) { + throw new KeeperSdkError( + `Folder key not found for ${folderUid}. Run sync() first.`, + ResultCodes.NSF_MISSING_KEY + ) + } + + const { encryptionType, keyType } = await resolveRecordKeyType(storage, recordUid, record?.version ?? 3) + const encryptedRecordKey = await platform.wrapKey(recordUid, folderUid, encryptionType, storage) + return { + recordUid: normal64Bytes(recordUid), + encryptedRecordKey, + encryptedRecordKeyType: keyType, + } +} + +function parseFolderRecordUpdateResponse( + response: Folder.IFolderRecordUpdateResponse, + folderUid: string, + recordUid: string +): LinkNsfRecordResult { + const result = response.folderRecordUpdateResult?.[0] + if (!result) { + return { + success: true, + recordUid, + folderUid, + status: 'SUCCESS', + message: 'Record added to folder successfully', + } + } + const statusName = Folder.FolderModifyStatus[result.status ?? Folder.FolderModifyStatus.SUCCESS] ?? 'UNKNOWN' + const success = result.status === Folder.FolderModifyStatus.SUCCESS + return { + success, + recordUid: result.recordUid?.length ? webSafe64FromBytes(result.recordUid) : recordUid, + folderUid: response.folderUid?.length ? webSafe64FromBytes(response.folderUid) : folderUid, + status: statusName, + message: result.message || (success ? 'Record added to folder successfully' : 'Failed to link record'), + } +} + +export async function linkNestedShareRecord( + storage: InMemoryStorage, + auth: Auth, + recordIdentifier: string, + folderIdentifier: string +): Promise { + const recordUid = resolveNsfRecordIdentifier(storage, recordIdentifier) + if (!recordUid) { + throw new KeeperSdkError(`Record '${recordIdentifier}' not found`, ResultCodes.NSF_NOT_FOUND) + } + + const folderUid = resolveNsfFolderIdentifier(storage, folderIdentifier) + if (!folderUid) { + throw new KeeperSdkError(`Folder '${folderIdentifier}' not found`, ResultCodes.NSF_NOT_FOUND) + } + + ensureNestedShareRecord(storage, recordUid, recordIdentifier) + ensureNestedShareFolder(storage, folderUid, folderIdentifier) + + try { + const recordMetadata = await buildRecordMetadata(storage, folderUid, recordUid) + const response = await auth.executeRest( + folderRecordUpdateMessage({ + folderUid: normal64Bytes(folderUid), + addRecords: [recordMetadata], + }) + ) + const parsed = parseFolderRecordUpdateResponse(response, folderUid, recordUid) + if (!parsed.success) { + throw new KeeperSdkError(parsed.message, ResultCodes.NSF_LINK_FAILED) + } + return parsed + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to link record to folder: ${extractErrorMessage(err)}`, + ResultCodes.NSF_LINK_FAILED + ) + } +} diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts index a071c71..53e9dcf 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -3,10 +3,20 @@ import type { DUser, DKdFolder, DKdFolderAccess, + DKdFolderRecord, + DKdRecordAccess, Dependency, } from '@keeper-security/keeperapi' import { Folder, webSafe64FromBytes } from '@keeper-security/keeperapi' import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { VaultObjectKind } from '../folders/folderHelpers' +import { KeeperSdkError, ResultCodes } from '../utils' +import { getRecordTitle } from '../records/RecordUtils' + +const LEGACY_RECORD_MSG = + "Record '{0}' is a legacy vault record. Nested Share Folder commands operate only on Nested Share Records." +const LEGACY_FOLDER_MSG = + "Folder '{0}' is a legacy folder. Nested Share Folder commands operate only on Nested Share Folders." const ACCESS_ROLE_LABELS: Record = { [Folder.AccessRoleType.NAVIGATOR]: 'navigator', @@ -46,6 +56,153 @@ export enum NsfItemType { Record = 'Record', } +export function isNestedShareRecord(storage: InMemoryStorage, recordUid: string): boolean { + return !!getKeeperDriveRecord(storage, recordUid) +} + +export function isNestedShareFolder(storage: InMemoryStorage, folderUid: string): boolean { + if (!folderUid) return false + if (isRootFolderUid(folderUid)) return true + return !!getKeeperDriveFolder(storage, folderUid) +} + +export function ensureNestedShareRecord(storage: InMemoryStorage, recordUid: string, identifier?: string): void { + if (isNestedShareRecord(storage, recordUid)) return + const ident = identifier ?? recordUid + throw new KeeperSdkError(LEGACY_RECORD_MSG.replace('{0}', ident), ResultCodes.NSF_LEGACY_RECORD) +} + +export function ensureNestedShareFolder(storage: InMemoryStorage, folderUid: string, identifier?: string): void { + if (isNestedShareFolder(storage, folderUid)) return + const ident = identifier ?? folderUid + throw new KeeperSdkError(LEGACY_FOLDER_MSG.replace('{0}', ident), ResultCodes.NSF_LEGACY_FOLDER) +} + +function resolveByUidOrName( + items: T[], + identifier: string, + getUid: (item: T) => string, + getName: (item: T) => string +): T | undefined { + const trimmed = identifier.trim() + if (!trimmed) return undefined + + const byUid = items.find((item) => getUid(item) === trimmed) + if (byUid) return byUid + + const lower = trimmed.toLowerCase() + const nameMatches = items.filter((item) => getName(item).toLowerCase() === lower) + if (nameMatches.length === 1) return nameMatches[0] + if (nameMatches.length > 1) { + throw new KeeperSdkError( + `Multiple matches found for "${identifier}". Use a UID instead.`, + ResultCodes.MULTIPLE_NSF_MATCHES + ) + } + return undefined +} + +function resolveRecordByTitleSearch(storage: InMemoryStorage, identifier: string): DRecord | undefined { + const lower = identifier.toLowerCase() + const matches = getKeeperDriveRecords(storage).filter((record) => { + const title = getRecordTitle(record) + return title && lower.length > 0 && title.toLowerCase().includes(lower) + }) + if (matches.length === 1) return matches[0] + if (matches.length > 1) { + throw new KeeperSdkError( + `Multiple records matched "${identifier}". Use a UID instead.`, + ResultCodes.MULTIPLE_NSF_MATCHES + ) + } + return undefined +} + +function resolveFolderByPath(storage: InMemoryStorage, identifier: string): string | undefined { + const trimmed = identifier.trim().replace(/^\/+/, '') + if (!trimmed) return ROOT_FOLDER_UID + + const targetPath = `/${trimmed.toLowerCase()}` + for (const folder of getKeeperDriveFolders(storage)) { + if (buildFolderPath(storage, folder.uid).toLowerCase() === targetPath) { + return folder.uid + } + } + return undefined +} + +export function resolveNsfRecordIdentifier(storage: InMemoryStorage, identifier: string): string | undefined { + const trimmed = identifier.trim() + if (!trimmed) return undefined + + const kdRecord = getKeeperDriveRecord(storage, trimmed) + if (kdRecord) return kdRecord.uid + + const anyRecord = storage.getByUid(VaultObjectKind.Record, trimmed) + if (anyRecord) return anyRecord.uid + + return resolveRecordByTitleSearch(storage, trimmed)?.uid +} + +export function resolveNsfFolderIdentifier(storage: InMemoryStorage, identifier: string): string | undefined { + const trimmed = identifier.trim() + if (!trimmed) return undefined + + if (isRootFolderUid(trimmed) || trimmed.toLowerCase() === 'root') return ROOT_FOLDER_UID + + const byUidOrName = resolveByUidOrName( + getKeeperDriveFolders(storage), + trimmed, + (folder) => folder.uid, + (folder) => folder.data.name || '' + ) + if (byUidOrName) return byUidOrName.uid + + return resolveFolderByPath(storage, trimmed) +} + +export function findNestedShareFoldersForRecord(storage: InMemoryStorage, recordUid: string): string[] { + return storage + .getAll(KeeperDriveKind.FolderRecord) + .filter((entry) => entry.recordUid === recordUid) + .map((entry) => entry.folderUid) +} + +export function checkRecordDeletePermission( + storage: InMemoryStorage, + recordUid: string, + username: string, + accountUid?: Uint8Array +): void { + const entries = storage + .getAll(KeeperDriveKind.RecordAccess) + .filter((entry) => entry.recordUid === recordUid) + if (entries.length === 0) return + + const accountUidStr = accountUid?.length ? webSafe64FromBytes(accountUid) : '' + for (const entry of entries) { + const isCurrentUser = + (entry.accessType === Folder.AccessType.AT_USER && + entry.accessTypeUid === accountUidStr) || + (username && + storage.getAll('user').some( + (user) => + user.username === username && + webSafe64FromBytes(user.accountUid) === entry.accessTypeUid + )) + if (!isCurrentUser) continue + if (entry.owner || entry.canDelete) return + throw new KeeperSdkError( + 'You do not have permission to delete this record.', + ResultCodes.NSF_PERMISSION_DENIED + ) + } + throw new KeeperSdkError( + 'You do not have permission to delete this record.', + ResultCodes.NSF_PERMISSION_DENIED + ) +} + export function formatAccessRoleType(role: Folder.AccessRoleType | null | undefined): string { if (role == null) return 'unknown' return ACCESS_ROLE_LABELS[role] ?? `role-${role}` diff --git a/KeeperSdk/src/nestedShareFolders/nsfRemoveMessages.ts b/KeeperSdk/src/nestedShareFolders/nsfRemoveMessages.ts new file mode 100644 index 0000000..0bdf82c --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/nsfRemoveMessages.ts @@ -0,0 +1,257 @@ +import type { RestMessage } from '@keeper-security/keeperapi' +import { Reader, Writer } from 'protobufjs' + +const REMOVE_RECORD_PATH = 'vault/folders/v3/remove_record' + +function readBytes(reader: Reader): Uint8Array { + return reader.bytes() as Uint8Array +} + +export enum RemoveRecordAction { + Preview = 0, + Confirm = 1, +} + +export enum RemoveRecordOperation { + Unlink = 1, + FolderTrash = 2, + OwnerTrash = 3, +} + +export enum RemoveRecordStatus { + Unknown = 0, + Success = 1, + StalePreview = 2, + TokenExpired = 3, + TokenInvalid = 4, + AccessDenied = 5, + ValidationError = 6, +} + +export type RemoveRecordRemovalInput = { + recordUid: Uint8Array + folderUid?: Uint8Array + operation: RemoveRecordOperation +} + +export type RemoveRecordRequest = { + action: RemoveRecordAction + records: RemoveRecordRemovalInput[] + confirmationToken?: Uint8Array +} + +export type RemoveRecordImpact = { + foldersCount: number + recordsCount: number + affectedUsersCount: number + affectedTeamsCount: number + recordInfo: { recordUid: Uint8Array; locationsCount: number }[] + warnings: string[] +} + +export type RemoveRecordItemError = { + code: number + message: string +} + +export type RemoveRecordResultItem = { + recordUid: Uint8Array + folderUid: Uint8Array + status: RemoveRecordStatus + impact?: RemoveRecordImpact + error?: RemoveRecordItemError +} + +export type RemoveRecordResponse = { + confirmationToken: Uint8Array + tokenExpiresAt?: number + results: RemoveRecordResultItem[] + errorMessage?: string +} + +function longToNumber(value: number | { toNumber: () => number } | null | undefined): number | undefined { + if (value == null) return undefined + return typeof value === 'number' ? value : value.toNumber() +} + +function encodeRecordRemoval(item: RemoveRecordRemovalInput): Uint8Array { + const writer = Writer.create() + if (item.folderUid?.length) writer.uint32(10).bytes(item.folderUid) + writer.uint32(18).bytes(item.recordUid) + writer.uint32(24).int32(item.operation) + return writer.finish() +} + +function encodeRemoveRecordRequest(request: RemoveRecordRequest): Uint8Array { + const writer = Writer.create() + writer.uint32(8).int32(request.action) + for (const record of request.records) { + writer.uint32(18).bytes(encodeRecordRemoval(record)) + } + if (request.confirmationToken?.length) { + writer.uint32(26).bytes(request.confirmationToken) + } + return writer.finish() +} + +function skipUnknownField(reader: Reader, wireType: number): void { + reader.skipType(wireType) +} + +function decodeImpact(reader: Reader, length: number): RemoveRecordImpact { + const end = reader.pos + length + const impact: RemoveRecordImpact = { + foldersCount: 0, + recordsCount: 0, + affectedUsersCount: 0, + affectedTeamsCount: 0, + recordInfo: [], + warnings: [], + } + while (reader.pos < end) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: + impact.foldersCount = reader.int32() + break + case 2: + impact.recordsCount = reader.int32() + break + case 3: + impact.affectedUsersCount = reader.int32() + break + case 4: + impact.affectedTeamsCount = reader.int32() + break + case 5: { + const subLen = reader.uint32() + const subEnd = reader.pos + subLen + let recordUid: Uint8Array = new Uint8Array(0) + let locationsCount = 0 + while (reader.pos < subEnd) { + const subTag = reader.uint32() + switch (subTag >>> 3) { + case 1: + recordUid = readBytes(reader) + break + case 2: + locationsCount = reader.int32() + break + default: + skipUnknownField(reader, subTag & 7) + break + } + } + impact.recordInfo.push({ recordUid, locationsCount }) + break + } + case 6: + impact.warnings.push(reader.string()) + break + default: + skipUnknownField(reader, tag & 7) + break + } + } + return impact +} + +function decodeItemError(reader: Reader, length: number): RemoveRecordItemError { + const end = reader.pos + length + let code = 0 + let message = '' + while (reader.pos < end) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: + code = reader.int32() + break + case 2: + message = reader.string() + break + default: + skipUnknownField(reader, tag & 7) + break + } + } + return { code, message } +} + +function decodeRemoveResult(reader: Reader, length: number): RemoveRecordResultItem { + const end = reader.pos + length + const item: RemoveRecordResultItem = { + recordUid: new Uint8Array(0), + folderUid: new Uint8Array(0), + status: RemoveRecordStatus.Unknown, + } + while (reader.pos < end) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: + item.recordUid = readBytes(reader) + break + case 2: + item.folderUid = readBytes(reader) + break + case 3: + item.status = reader.int32() as RemoveRecordStatus + break + case 4: + item.impact = decodeImpact(reader, reader.uint32()) + break + case 5: + item.error = decodeItemError(reader, reader.uint32()) + break + default: + skipUnknownField(reader, tag & 7) + break + } + } + return item +} + +function decodeRemoveRecordResponse(data: Uint8Array): RemoveRecordResponse { + const reader = Reader.create(data) + const response: RemoveRecordResponse = { + confirmationToken: new Uint8Array(0), + results: [], + } + while (reader.pos < reader.len) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: + response.confirmationToken = readBytes(reader) + break + case 2: + response.tokenExpiresAt = longToNumber( + reader.int64() as unknown as number | { toNumber: () => number } + ) + break + case 3: + response.results.push(decodeRemoveResult(reader, reader.uint32())) + break + case 4: + response.errorMessage = reader.string() + break + default: + skipUnknownField(reader, tag & 7) + break + } + } + return response +} + +export function removeRecordMessage( + request: RemoveRecordRequest +): RestMessage { + return { + path: REMOVE_RECORD_PATH, + data: request, + toBytes(): Uint8Array { + return encodeRemoveRecordRequest(request) + }, + fromBytes(data: Uint8Array): RemoveRecordResponse { + return decodeRemoveRecordResponse(data) + }, + } +} diff --git a/KeeperSdk/src/nestedShareFolders/removeNsfRecord.ts b/KeeperSdk/src/nestedShareFolders/removeNsfRecord.ts new file mode 100644 index 0000000..5a1cad1 --- /dev/null +++ b/KeeperSdk/src/nestedShareFolders/removeNsfRecord.ts @@ -0,0 +1,247 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { normal64Bytes, webSafe64FromBytes } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../storage/InMemoryStorage' +import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../utils' +import { + checkRecordDeletePermission, + ensureNestedShareRecord, + findNestedShareFoldersForRecord, + resolveNsfFolderIdentifier, + resolveNsfRecordIdentifier, +} from './nsfHelpers' +import { + RemoveRecordAction, + RemoveRecordOperation, + RemoveRecordStatus, + removeRecordMessage, + type RemoveRecordRemovalInput, + type RemoveRecordResponse, + type RemoveRecordResultItem, +} from './nsfRemoveMessages' + +const MAX_REMOVALS = 500 + +export enum NsfRemoveOperation { + OwnerTrash = 'owner-trash', + FolderTrash = 'folder-trash', + Unlink = 'unlink', +} + +export type NsfRemoveOperationInput = NsfRemoveOperation | `${NsfRemoveOperation}` + +export type RemoveNsfRecordInput = { + records: string[] + folder?: string + operation?: NsfRemoveOperationInput + force?: boolean + dryRun?: boolean +} + +export type NsfRemovePreviewItem = { + recordUid: string + folderUid: string + status: string + impact?: { + foldersCount: number + recordsCount: number + affectedUsersCount: number + affectedTeamsCount: number + warnings: string[] + } + error?: { code: number; message: string } +} + +export type RemoveNsfRecordResult = { + confirmed: boolean + dryRun: boolean + preview: NsfRemovePreviewItem[] + message?: string +} + +const OPERATION_MAP: Record = { + [NsfRemoveOperation.Unlink]: RemoveRecordOperation.Unlink, + [NsfRemoveOperation.FolderTrash]: RemoveRecordOperation.FolderTrash, + [NsfRemoveOperation.OwnerTrash]: RemoveRecordOperation.OwnerTrash, +} + +function normalizeOperation(operation: NsfRemoveOperationInput = NsfRemoveOperation.OwnerTrash): NsfRemoveOperation { + const value = operation as NsfRemoveOperation + if (value in OPERATION_MAP) return value + throw new KeeperSdkError( + `Invalid operation '${operation}'. Use: owner-trash, folder-trash, unlink.`, + ResultCodes.NSF_REMOVE_FAILED + ) +} + +function mapPreviewItem(item: RemoveRecordResultItem): NsfRemovePreviewItem { + return { + recordUid: item.recordUid.length ? webSafe64FromBytes(item.recordUid) : '', + folderUid: item.folderUid.length ? webSafe64FromBytes(item.folderUid) : '', + status: RemoveRecordStatus[item.status] ?? String(item.status), + impact: item.impact + ? { + foldersCount: item.impact.foldersCount, + recordsCount: item.impact.recordsCount, + affectedUsersCount: item.impact.affectedUsersCount, + affectedTeamsCount: item.impact.affectedTeamsCount, + warnings: [...item.impact.warnings], + } + : undefined, + error: item.error, + } +} + +function mapPreview(response: RemoveRecordResponse): NsfRemovePreviewItem[] { + return response.results.map(mapPreviewItem) +} + +function hasPreviewErrors(preview: NsfRemovePreviewItem[]): boolean { + return preview.some((item) => item.error != null || item.status !== 'Success') +} + +type RemovalSpec = { + recordUid: string + folderUid?: string + operation: RemoveRecordOperation +} + +function buildRemovals( + storage: InMemoryStorage, + auth: Auth, + recordIdentifiers: string[], + folderIdentifier: string | undefined, + operation: NsfRemoveOperation +): RemovalSpec[] { + if (recordIdentifiers.length === 0) { + throw new KeeperSdkError('At least one record UID or title is required.', ResultCodes.NSF_NOT_FOUND) + } + if (recordIdentifiers.length > MAX_REMOVALS) { + throw new KeeperSdkError(`Maximum ${MAX_REMOVALS} records per request.`, ResultCodes.NSF_TOO_MANY_RECORDS) + } + if (operation === NsfRemoveOperation.Unlink && !folderIdentifier?.trim()) { + throw new KeeperSdkError( + '--folder is required when operation is "unlink".', + ResultCodes.NSF_FOLDER_REQUIRED + ) + } + + const folderUid = folderIdentifier ? resolveNsfFolderIdentifier(storage, folderIdentifier) : undefined + if (folderIdentifier && !folderUid) { + throw new KeeperSdkError(`Folder '${folderIdentifier}' not found`, ResultCodes.NSF_NOT_FOUND) + } + + const removals: RemovalSpec[] = [] + for (const identifier of recordIdentifiers) { + const recordUid = resolveNsfRecordIdentifier(storage, identifier) + if (!recordUid) { + throw new KeeperSdkError(`Record '${identifier}' not found`, ResultCodes.NSF_NOT_FOUND) + } + ensureNestedShareRecord(storage, recordUid, identifier) + checkRecordDeletePermission(storage, recordUid, auth.username, auth.accountUid) + + let ctxFolder = folderUid + if (!ctxFolder) { + const folders = findNestedShareFoldersForRecord(storage, recordUid) + if (folders.length === 0 && operation !== NsfRemoveOperation.OwnerTrash) { + throw new KeeperSdkError( + `No folder context for record '${identifier}'. Use folder option or owner-trash operation.`, + ResultCodes.NSF_NOT_FOUND + ) + } + ctxFolder = folders[0] + } + + removals.push({ + recordUid, + folderUid: ctxFolder, + operation: OPERATION_MAP[operation], + }) + } + return removals +} + +function toRemovalInput(spec: RemovalSpec): RemoveRecordRemovalInput { + return { + recordUid: normal64Bytes(spec.recordUid), + folderUid: spec.folderUid ? normal64Bytes(spec.folderUid) : undefined, + operation: spec.operation, + } +} + +async function executeRemove( + auth: Auth, + removals: RemovalSpec[], + action: RemoveRecordAction, + confirmationToken?: Uint8Array +): Promise { + return auth.executeRest( + removeRecordMessage({ + action, + records: removals.map(toRemovalInput), + confirmationToken, + }) + ) +} + +export function formatRemoveNsfPreview(preview: NsfRemovePreviewItem[]): string { + const lines: string[] = [] + for (const item of preview) { + lines.push(`Record: ${item.recordUid}`) + if (item.folderUid) lines.push(` Folder: ${item.folderUid}`) + lines.push(` Status: ${item.status}`) + if (item.impact) { + lines.push( + ` Impact: folders=${item.impact.foldersCount}, records=${item.impact.recordsCount}, users=${item.impact.affectedUsersCount}, teams=${item.impact.affectedTeamsCount}` + ) + for (const warning of item.impact.warnings) { + lines.push(` Warning: ${warning}`) + } + } + if (item.error?.message) { + lines.push(` Error: ${item.error.message}`) + } + lines.push('') + } + return lines.join('\n').trimEnd() +} + +export async function removeNestedShareRecords( + storage: InMemoryStorage, + auth: Auth, + input: RemoveNsfRecordInput +): Promise { + const operation = normalizeOperation(input.operation) + const dryRun = input.dryRun ?? false + const removals = buildRemovals(storage, auth, input.records, input.folder, operation) + + try { + const previewResponse = await executeRemove(auth, removals, RemoveRecordAction.Preview) + const preview = mapPreview(previewResponse) + + if (hasPreviewErrors(preview)) { + throw new KeeperSdkError(formatRemoveNsfPreview(preview) || 'Removal preview failed.', ResultCodes.NSF_REMOVE_FAILED) + } + + if (dryRun || !previewResponse.confirmationToken?.length) { + return { confirmed: false, dryRun, preview } + } + + if (!input.force) { + return { confirmed: false, dryRun: false, preview, message: 'Confirmation required. Set force=true to proceed.' } + } + + await executeRemove(auth, removals, RemoveRecordAction.Confirm, previewResponse.confirmationToken) + return { + confirmed: true, + dryRun: false, + preview, + message: `Removed ${removals.length} record(s).`, + } + } catch (err) { + if (err instanceof KeeperSdkError) throw err + throw new KeeperSdkError( + `Failed to remove nested share record(s): ${extractErrorMessage(err)}`, + ResultCodes.NSF_REMOVE_FAILED + ) + } +} diff --git a/KeeperSdk/src/sharing/Sharing.ts b/KeeperSdk/src/sharing/Sharing.ts index 4dcc207..5b21ea3 100644 --- a/KeeperSdk/src/sharing/Sharing.ts +++ b/KeeperSdk/src/sharing/Sharing.ts @@ -4,7 +4,7 @@ import { Authentication, platform, getPublicKeysMessage, - keeperDriveRecordAccessByUid, + keeperDriveRecordAccessMessage, webSafe64FromBytes, recordsShareUpdateMessage, normal64Bytes, @@ -243,7 +243,7 @@ function longToNumber(value: number | { toNumber: () => number } | null | undefi export async function getRecordShareInfo(auth: Auth, recordUid: string): Promise { let response try { - response = await auth.executeRest(keeperDriveRecordAccessByUid(recordUid)) + response = await auth.executeRest(keeperDriveRecordAccessMessage([normal64Bytes(recordUid)])) } catch (err) { throw new KeeperSdkError( `Failed to fetch share info for ${recordUid}: ${extractErrorMessage(err)}` diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index 2aca283..dfa22be 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -57,6 +57,14 @@ export enum RoleErrorCode { export enum NsfErrorCode { NotFound = 'nsf_not_found', MultipleMatches = 'nsf_multiple_matches', + LegacyRecord = 'nsf_legacy_record', + LegacyFolder = 'nsf_legacy_folder', + PermissionDenied = 'nsf_permission_denied', + LinkFailed = 'nsf_link_failed', + RemoveFailed = 'nsf_remove_failed', + FolderRequired = 'nsf_folder_required', + TooManyRecords = 'nsf_too_many_records', + MissingKey = 'nsf_missing_key', } export enum TeamErrorCode { @@ -128,6 +136,14 @@ export const ResultCodes = { ROLE_ENFORCEMENT_FAILED: RoleErrorCode.RoleEnforcementFailed, NSF_NOT_FOUND: NsfErrorCode.NotFound, MULTIPLE_NSF_MATCHES: NsfErrorCode.MultipleMatches, + NSF_LEGACY_RECORD: NsfErrorCode.LegacyRecord, + NSF_LEGACY_FOLDER: NsfErrorCode.LegacyFolder, + NSF_PERMISSION_DENIED: NsfErrorCode.PermissionDenied, + NSF_LINK_FAILED: NsfErrorCode.LinkFailed, + NSF_REMOVE_FAILED: NsfErrorCode.RemoveFailed, + NSF_FOLDER_REQUIRED: NsfErrorCode.FolderRequired, + NSF_TOO_MANY_RECORDS: NsfErrorCode.TooManyRecords, + NSF_MISSING_KEY: NsfErrorCode.MissingKey, TEAM_REQUIRED: TeamErrorCode.TeamRequired, TEAM_NOT_FOUND: TeamErrorCode.TeamNotFound, MULTIPLE_TEAM_MATCHES: TeamErrorCode.MultipleTeamMatches, diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index aa1976c..63ad59b 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -78,6 +78,8 @@ import { UserManager } from '../users/UserManager' import { NestedShareFolderManager } from '../nestedShareFolders/NestedShareFolderManager' import type { ListNsfOptions, ListNsfRow, ListNsfFormatInput, FormattedListNsfTable } from '../nestedShareFolders/listNsf' import type { GetNsfOptions, GetNsfResult } from '../nestedShareFolders/getNsf' +import type { LinkNsfRecordResult } from '../nestedShareFolders/linkNsfRecord' +import type { RemoveNsfRecordInput, RemoveNsfRecordResult } from '../nestedShareFolders/removeNsfRecord' import type { ListUserRow, ListUsersOptions, @@ -730,6 +732,25 @@ export class KeeperVault { return this.nestedShareFolderManager.formatNsfDetail(result, verbose ?? false) } + public async linkNestedShareRecord( + recordIdentifier: string, + folderIdentifier: string + ): Promise { + const result = await this.nestedShareFolderManager.linkNestedShareRecord(recordIdentifier, folderIdentifier) + if (result.success) await this.syncIfNeeded() + return result + } + + public async removeNestedShareRecords(input: RemoveNsfRecordInput): Promise { + const result = await this.nestedShareFolderManager.removeNestedShareRecords(input) + if (result.confirmed) await this.syncIfNeeded() + return result + } + + public formatRemoveNsfPreview(preview: RemoveNsfRecordResult['preview']): string { + return this.nestedShareFolderManager.formatRemoveNsfPreview(preview) + } + public async shareFolder(input: ShareFolderInput): Promise { const result = await this.sharedFolderManager.shareFolder(input) if (result.success) await this.syncIfNeeded() @@ -755,6 +776,16 @@ export class KeeperVault { return this.getAuthOrThrow() } + /** Closes the push websocket while keeping the REST session active. */ + public releasePushConnection(): void { + if (!this.auth) return + try { + this.auth.disconnect() + } catch (err) { + this.log.debug('releasePushConnection error:', extractErrorMessage(err)) + } + } + public disconnect(): void { if (this.auth) { try { diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index 268c9f5..165e6c7 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -31,6 +31,8 @@ "roles:delete": "ts-node src/roles/deleteRole.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", + "nsf:rm": "ts-node src/nestedShareFolders/remove_nsf.ts", "teams:list": "ts-node src/teams/list_teams.ts", "teams:view": "ts-node src/teams/view_team.ts", "teams:add": "ts-node src/teams/add_team.ts", diff --git a/examples/sdk_example/src/nestedShareFolders/link_nsf.ts b/examples/sdk_example/src/nestedShareFolders/link_nsf.ts new file mode 100644 index 0000000..896ed6b --- /dev/null +++ b/examples/sdk_example/src/nestedShareFolders/link_nsf.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 linkNsf() { + const vault = await login() + + try { + const recordIdentifier = (await prompt('Record UID or title: ')).trim() + const folderIdentifier = (await prompt('Destination folder UID or name: ')).trim() + if (!recordIdentifier || !folderIdentifier) { + logger.info('Both record and folder are required.') + return + } + + const restore = suppressLogs() + let result + try { + result = await vault.linkNestedShareRecord(recordIdentifier, folderIdentifier) + } finally { + restore() + } + + logger.info('') + logger.info(result.message) + logger.info(`Record: ${result.recordUid}`) + logger.info(`Folder: ${result.folderUid}`) + logger.info(`Status: ${result.status}`) + logger.info('') + } catch (err) { + logger.error(`Link failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(linkNsf) diff --git a/examples/sdk_example/src/nestedShareFolders/remove_nsf.ts b/examples/sdk_example/src/nestedShareFolders/remove_nsf.ts new file mode 100644 index 0000000..d4ce1c7 --- /dev/null +++ b/examples/sdk_example/src/nestedShareFolders/remove_nsf.ts @@ -0,0 +1,119 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + NsfRemoveOperation, + prompt, + suppressLogs, + type RemoveNsfRecordInput, + type RemoveNsfRecordResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../utils/runner' +import { isYes } from '../utils/format' + +const OPERATION_BY_INPUT: Record = { + '': NsfRemoveOperation.OwnerTrash, + '1': NsfRemoveOperation.OwnerTrash, + '2': NsfRemoveOperation.FolderTrash, + '3': NsfRemoveOperation.Unlink, + 'owner-trash': NsfRemoveOperation.OwnerTrash, + 'folder-trash': NsfRemoveOperation.FolderTrash, + unlink: NsfRemoveOperation.Unlink, +} + +function parseOperation(input: string): NsfRemoveOperation { + return OPERATION_BY_INPUT[input.trim().toLowerCase()] ?? NsfRemoveOperation.OwnerTrash +} + +function printPreview(vault: Awaited>, result: RemoveNsfRecordResult): void { + if (result.preview.length === 0) return + logger.info('') + logger.info(vault.formatRemoveNsfPreview(result.preview)) + logger.info('') +} + +function printPreviewWarnings(result: RemoveNsfRecordResult): void { + for (const item of result.preview) { + for (const warning of item.impact?.warnings ?? []) { + logger.info(`Warning: ${warning}`) + } + } +} + +async function removeNestedShareRecords( + vault: Awaited>, + input: RemoveNsfRecordInput +): Promise { + const restore = suppressLogs() + try { + return await vault.removeNestedShareRecords(input) + } finally { + restore() + } +} + +async function removeNsf() { + const vault = await login() + + try { + const recordsInput = (await prompt('Record UID(s) or title(s), comma-separated: ')).trim() + const records = recordsInput.split(',').map((value) => value.trim()).filter(Boolean) + if (records.length === 0) { + logger.info('At least one record is required.') + return + } + + logger.info('Operation: 1) owner-trash 2) folder-trash 3) unlink') + const operation = parseOperation(await prompt('Choose [1]: ')) + const folder = + operation === NsfRemoveOperation.Unlink + ? (await prompt('Folder UID or name (required for unlink): ')).trim() + : (await prompt('Folder UID or name (optional): ')).trim() + const dryRun = isYes(await prompt('Dry run (preview only)? [y/N]: ')) + const force = dryRun ? false : isYes(await prompt('Force confirm without prompt? [y/N]: ')) + + const baseInput: RemoveNsfRecordInput = { + records, + folder: folder || undefined, + operation, + } + + if (dryRun) { + const result = await removeNestedShareRecords(vault, { ...baseInput, dryRun: true }) + printPreview(vault, result) + logger.info('[Dry-run] No records were removed.') + return + } + + if (force) { + const result = await removeNestedShareRecords(vault, { ...baseInput, force: true }) + printPreview(vault, result) + if (result.confirmed && result.message) { + logger.info(result.message) + } + return + } + + const preview = await removeNestedShareRecords(vault, { ...baseInput, force: false }) + printPreview(vault, preview) + printPreviewWarnings(preview) + + if (!isYes(await prompt('Do you want to proceed with deletion? [y/n]: '))) { + logger.info('Removal cancelled.') + return + } + + const result = await removeNestedShareRecords(vault, { ...baseInput, force: true }) + if (result.confirmed && result.message) { + logger.info(result.message) + } + } catch (err) { + logger.error(`Remove failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(removeNsf) diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index a348e40..8746b50 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -481,6 +481,16 @@ export const getSharingAdminsMessage = ( Enterprise.GetSharingAdminsResponse ) +export const folderRecordUpdateMessage = ( + data: Folder.IFolderRecordUpdateRequest +): RestMessage => + createMessage( + data, + 'vault/folders/v3/record_update', + Folder.FolderRecordUpdateRequest, + Folder.FolderRecordUpdateResponse + ) + export const recordsAddMessage = ( data: Records.IRecordsAddRequest ): RestMessage =>