From ab2f3d0645ffba30ea0025d118f1ed6df73c3e5d Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Wed, 15 Jul 2026 18:25:43 +0530 Subject: [PATCH 1/4] code quality changes and few permission checks are added for NSF --- KeeperSdk/src/index.ts | 8 +++ KeeperSdk/src/nestedShareFolders/index.ts | 8 +++ .../src/nestedShareFolders/nsfConstants.ts | 62 +++++++++++++------ .../src/nestedShareFolders/nsfHelpers.ts | 15 ++--- .../nestedShareFolders/nsfRecordPermission.ts | 13 ++-- KeeperSdk/src/nestedShareFolders/nsfShare.ts | 48 ++++++++------ .../src/nestedShareFolders/nsfShortcut.ts | 12 ++-- .../src/nestedShareFolders/nsfTeamShare.ts | 44 ++----------- .../nestedShareFolders/nsfTransferRecord.ts | 6 +- KeeperSdk/src/nestedShareFolders/nsfTypes.ts | 58 ++++++++++++----- KeeperSdk/src/vault/KeeperVault.ts | 2 +- 11 files changed, 161 insertions(+), 115 deletions(-) diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index c126903..17ee6f5 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -580,9 +580,17 @@ export { transferNestedShareRecords, formatTransferNestedShareRecordResults, NSF_RECORD_PERMISSION_ROLES, + NSFShareRoleName, + NsfShareCommandName, + NSF_SHARE_EXPIRATION_NEVER, NsfFolderShareAction, + NsfFolderShareActionTaken, NsfRecordShareAction, + NsfRecordShareActionTaken, NsfRecordPermissionAction, + NsfResultStatus, + NsfTransferApiStatus, + NsfPermissionFailureCode, collectNsfRecordUidsInFolder, updateNestedShareRecordPermissions, buildNsfRecordPermissionPlan, diff --git a/KeeperSdk/src/nestedShareFolders/index.ts b/KeeperSdk/src/nestedShareFolders/index.ts index d1ccd63..da1fd6e 100644 --- a/KeeperSdk/src/nestedShareFolders/index.ts +++ b/KeeperSdk/src/nestedShareFolders/index.ts @@ -66,8 +66,13 @@ export { NsfRemoveFolderOperation, GetNsfRecordDetailsFormat, NsfFolderShareAction, + NsfFolderShareActionTaken, NsfRecordShareAction, + NsfRecordShareActionTaken, NsfRecordPermissionAction, + NsfResultStatus, + NsfTransferApiStatus, + NsfPermissionFailureCode, NSF_ACCESS_ROLE_LABELS, resolveRecordPermissionRole, toNsfAccessRoleLabel, @@ -170,7 +175,10 @@ export { NSF_FOLDER_COLORS, NSF_MAX_RECORD_BATCH, MIN_SHARE_EXPIRATION_MS, + NSF_SHARE_EXPIRATION_NEVER, TeamGetKeysResponseKeyType, + NSFShareRoleName, + NsfShareCommandName, } from './nsfConstants' export type { NsfFolderColor } from './nsfConstants' diff --git a/KeeperSdk/src/nestedShareFolders/nsfConstants.ts b/KeeperSdk/src/nestedShareFolders/nsfConstants.ts index dc23bcb..546a1b7 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfConstants.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfConstants.ts @@ -1,4 +1,5 @@ import { Folder } from '@keeper-security/keeperapi' +import { KeeperSdkError, ResultCodes } from '../utils' export const NSF_LEGACY_RECORD_MSG = "Record '{0}' is a legacy vault record. Nested Share Folder commands operate only on Nested Share Records." @@ -86,41 +87,58 @@ export const NSF_STRUCTURED_SUBKEYS: Record> = { phone: new Set(['number', 'region', 'type', 'ext']), } +export enum NSFShareRoleName { + Contributor = 'contributor', + Viewer = 'viewer', + ShareManager = 'share-manager', + ContentManager = 'content-manager', + ContentShareManager = 'content-share-manager', + FullManager = 'full-manager', + Unresolved = 'unresolved', +} + export const NSF_RECORD_PERMISSION_ROLES = [ - 'viewer', - 'share-manager', - 'content-manager', - 'content-share-manager', - 'full-manager', + NSFShareRoleName.Viewer, + NSFShareRoleName.ShareManager, + NSFShareRoleName.ContentManager, + NSFShareRoleName.ContentShareManager, + NSFShareRoleName.FullManager, ] as const export type NsfRecordPermissionRole = (typeof NSF_RECORD_PERMISSION_ROLES)[number] export const NSF_RECORD_PERMISSION_ROLE_LABELS: Record = { - [Folder.AccessRoleType.NAVIGATOR]: 'contributor', - [Folder.AccessRoleType.REQUESTOR]: 'contributor', - [Folder.AccessRoleType.VIEWER]: 'viewer', - [Folder.AccessRoleType.SHARED_MANAGER]: 'share-manager', - [Folder.AccessRoleType.CONTENT_MANAGER]: 'content-manager', - [Folder.AccessRoleType.CONTENT_SHARE_MANAGER]: 'content-share-manager', - [Folder.AccessRoleType.MANAGER]: 'full-manager', - [Folder.AccessRoleType.UNRESOLVED]: 'unresolved', + [Folder.AccessRoleType.NAVIGATOR]: NSFShareRoleName.Contributor, + [Folder.AccessRoleType.REQUESTOR]: NSFShareRoleName.Contributor, + [Folder.AccessRoleType.VIEWER]: NSFShareRoleName.Viewer, + [Folder.AccessRoleType.SHARED_MANAGER]: NSFShareRoleName.ShareManager, + [Folder.AccessRoleType.CONTENT_MANAGER]: NSFShareRoleName.ContentManager, + [Folder.AccessRoleType.CONTENT_SHARE_MANAGER]: NSFShareRoleName.ContentShareManager, + [Folder.AccessRoleType.MANAGER]: NSFShareRoleName.FullManager, + [Folder.AccessRoleType.UNRESOLVED]: NSFShareRoleName.Unresolved, } export const NSF_RECORD_PERMISSION_ROLE_MAP: Record = { - contributor: Folder.AccessRoleType.REQUESTOR, + [NSFShareRoleName.Contributor]: Folder.AccessRoleType.REQUESTOR, requestor: Folder.AccessRoleType.REQUESTOR, - viewer: Folder.AccessRoleType.VIEWER, - 'share-manager': Folder.AccessRoleType.SHARED_MANAGER, + [NSFShareRoleName.Viewer]: Folder.AccessRoleType.VIEWER, + [NSFShareRoleName.ShareManager]: Folder.AccessRoleType.SHARED_MANAGER, share_manager: Folder.AccessRoleType.SHARED_MANAGER, shared_manager: Folder.AccessRoleType.SHARED_MANAGER, - 'content-manager': Folder.AccessRoleType.CONTENT_MANAGER, + [NSFShareRoleName.ContentManager]: Folder.AccessRoleType.CONTENT_MANAGER, content_manager: Folder.AccessRoleType.CONTENT_MANAGER, - 'content-share-manager': Folder.AccessRoleType.CONTENT_SHARE_MANAGER, + [NSFShareRoleName.ContentShareManager]: Folder.AccessRoleType.CONTENT_SHARE_MANAGER, content_share_manager: Folder.AccessRoleType.CONTENT_SHARE_MANAGER, - 'full-manager': Folder.AccessRoleType.MANAGER, + [NSFShareRoleName.FullManager]: Folder.AccessRoleType.MANAGER, full_manager: Folder.AccessRoleType.MANAGER, } +export const NSF_SHARE_EXPIRATION_NEVER = 'never' + +export enum NsfShareCommandName { + FolderShare = 'nsf-share-folder', + RecordShare = 'nsf-share-record', +} + export const NSF_SHARE_BATCH_SIZE = 200 export enum TeamGetKeysResponseKeyType { @@ -174,6 +192,7 @@ type FolderRolePermissionFlags = { canListFolders?: boolean } +/** Role → FolderPermissions flags sent with folderAccessAdds / folderAccessUpdates. */ const NSF_FOLDER_ROLE_PERMISSIONS: Record = { [Folder.AccessRoleType.NAVIGATOR]: { canListFolders: true, @@ -236,7 +255,10 @@ const NSF_FOLDER_ROLE_PERMISSIONS: Record = { export function getFolderPermissionsForRole(roleType: Folder.AccessRoleType): Folder.IFolderPermissions { const flags = NSF_FOLDER_ROLE_PERMISSIONS[roleType] if (!flags) { - throw new Error(`Unknown folder access role type: ${roleType}`) + throw new KeeperSdkError( + `Unknown folder access role type: ${roleType}`, + ResultCodes.NSF_SHARE_FAILED + ) } return Folder.FolderPermissions.create(flags) } diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts index 369d860..0e6404d 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -36,6 +36,8 @@ import { NSF_TLA_EXPIRATION_TOLERANCE_MS, NSF_SHARE_EXPIRATION_RE, NSF_SHARE_EXPIRATION_UNIT_MS, + NSF_SHARE_EXPIRATION_NEVER, + NsfShareCommandName, } from './nsfConstants' import { NsfAccessRoleLabel, NSF_ACCESS_ROLE_LABELS, type ParseShareExpirationInput } from './nsfTypes' @@ -485,6 +487,7 @@ type FolderPermissionFlag = | 'canDelete' | 'canUpdateAccess' | 'canEditRecords' + | 'canUpdateSetting' | 'canChangeOwnership' function hasFolderPermission( @@ -807,7 +810,6 @@ export function resolveAccessUsername( return accessTypeUid } - export async function loadShareUserMap(auth: Auth, storage: InMemoryStorage): Promise> { const map = new Map() @@ -837,7 +839,6 @@ export async function loadShareUserMap(auth: Auth, storage: InMemoryStorage): Pr return map } - export function isFolderOwnerAccessor( folder: DKdFolder, entry: DKdFolderAccess, @@ -944,7 +945,7 @@ export function checkFolderEditPermission( username: string, accountUid: Uint8Array ): void { - if (hasFolderPermission(storage, folderUid, username, accountUid, 'canEditRecords')) return + if (hasFolderPermission(storage, folderUid, username, accountUid, 'canUpdateSetting')) return throw new KeeperSdkError( 'You do not have permission to edit this folder.', ResultCodes.NSF_PERMISSION_DENIED @@ -1043,7 +1044,7 @@ export type NsfRecordAccessFlags = { } export function getNsfAccessRoleLabel(access: NsfRecordAccessFlags): string { - if (access.owner) return 'owner' + if (access.owner) return NsfAccessRoleLabel.Owner return getNsfRecordPermissionRoleLabel(access.accessRoleType) } @@ -1211,7 +1212,7 @@ export function validateShareExpirationTimestamp( } export function parseShareExpiration(input: ParseShareExpirationInput): number | undefined { - const { expireAt, expireIn, expirationTimestamp, cmdName = 'nsf-share' } = input + const { expireAt, expireIn, expirationTimestamp, cmdName = NsfShareCommandName.FolderShare } = input const expireAtValue = expireAt?.trim() const expireInValue = expireIn?.trim() @@ -1237,7 +1238,7 @@ export function parseShareExpiration(input: ParseShareExpirationInput): number | const raw = expireAtValue || expireInValue if (!raw) return undefined - if (raw.toLowerCase() === 'never') return -1 + if (raw.toLowerCase() === NSF_SHARE_EXPIRATION_NEVER) return -1 if (expireAtValue) { const normalized = raw.replace('Z', '+00:00') @@ -1277,7 +1278,7 @@ export function parseShareExpiration(input: ParseShareExpirationInput): number | export function parseShareExpirationValue( value: string, - cmdName: string = 'nsf-share' + cmdName: string = NsfShareCommandName.FolderShare ): number | undefined { const raw = value.trim() if (!raw) return undefined diff --git a/KeeperSdk/src/nestedShareFolders/nsfRecordPermission.ts b/KeeperSdk/src/nestedShareFolders/nsfRecordPermission.ts index 3cd866f..3522c02 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfRecordPermission.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfRecordPermission.ts @@ -39,7 +39,7 @@ import type { UpdateNsfRecordPermissionInput, UpdateNsfRecordPermissionResult, } from './nsfTypes' -import { NsfRecordPermissionAction } from './nsfTypes' +import { NsfPermissionFailureCode, NsfRecordPermissionAction } from './nsfTypes' type NsfRecordAccessEntry = NsfLiveRecordAccessEntry @@ -371,9 +371,10 @@ function computePermissionChanges( const forbidden = new Set(forbiddenRecords) const ownerFlags = new Map() + const currentUserLower = currentUser.toLowerCase() for (const access of accesses) { - if (access.accessorName === currentUser) { + if (access.accessorName.toLowerCase() === currentUserLower) { ownerFlags.set(access.recordUid, access.owner || access.canUpdateAccess) } } @@ -394,7 +395,7 @@ function computePermissionChanges( if (!recordUids.has(recordUid) || access.owner) continue const email = access.accessorName - if (!email || email === currentUser) continue + if (!email || email.toLowerCase() === currentUserLower) continue const curRole = getNsfAccessRoleLabel(access) const isInherited = access.inherited @@ -550,7 +551,7 @@ export function formatNsfRecordPermissionPlan(plan: NsfRecordPermissionPlan): st ]) ) ) - lines.push('ALERT!!!') + lines.push('WARNING: Review the planned changes carefully before confirming.') lines.push('') } @@ -568,7 +569,7 @@ export function formatNsfRecordPermissionPlan(plan: NsfRecordPermissionPlan): st ]) ) ) - lines.push('ALERT!!!') + lines.push('WARNING: Review the planned changes carefully before confirming.') lines.push('') } @@ -584,7 +585,7 @@ function mapFailures( .map((outcome) => ({ recordUid: outcome.recordUid, email: outcome.email, - code: outcome.skipped ? 'skipped' : defaultCode, + code: outcome.skipped ? NsfPermissionFailureCode.Skipped : defaultCode, message: outcome.message || 'Unknown error', })) } diff --git a/KeeperSdk/src/nestedShareFolders/nsfShare.ts b/KeeperSdk/src/nestedShareFolders/nsfShare.ts index a080ed3..0f40dfa 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfShare.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfShare.ts @@ -20,9 +20,10 @@ import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../utils' import { collectNsfRecordUidsInFolder } from './nsfRecordPermission' import { resolveRecordKeyBytes } from './nsfRecordCrypto' import { transferNestedShareRecordOwnership } from './nsfTransferRecord' -import { getFolderPermissionsForRole } from './nsfConstants' +import { getFolderPermissionsForRole, NSFShareRoleName, NsfShareCommandName } from './nsfConstants' import { checkFolderSharePermission, + checkRecordChangeOwnershipPermission, checkRecordSharePermission, collectExistingFolderShareTargets, ensureNestedShareFolder, @@ -60,7 +61,13 @@ import type { ShareNestedShareRecordInput, ShareNestedShareRecordResult, } from './nsfTypes' -import { NsfFolderShareAction, NsfRecordShareAction } from './nsfTypes' +import { + NsfFolderShareAction, + NsfFolderShareActionTaken, + NsfRecordShareAction, + NsfRecordShareActionTaken, + NsfResultStatus, +} from './nsfTypes' const SHARE_ERROR = ResultCodes.NSF_SHARE_FAILED @@ -278,7 +285,7 @@ async function grantFolderAccess( recipient: target.recipient, isTeam: target.isTeam, success: true, - actionTaken: 'already_had_access', + actionTaken: NsfFolderShareActionTaken.AlreadyHadAccess, message: `Already has ${getNsfRecordPermissionRoleLabel(accessRoleType)} access`, } } @@ -311,7 +318,7 @@ async function grantFolderAccess( recipient: target.recipient, isTeam: target.isTeam, success: parsed.success, - actionTaken: existing != null ? 'updated' : 'granted', + actionTaken: existing != null ? NsfFolderShareActionTaken.Updated : NsfFolderShareActionTaken.Granted, message: parsed.message, } } @@ -344,7 +351,7 @@ async function removeFolderAccess( recipient: target.recipient, isTeam: target.isTeam, success: parsed.success, - actionTaken: 'removed', + actionTaken: NsfFolderShareActionTaken.Removed, message: parsed.message, } } @@ -368,7 +375,7 @@ export async function shareNestedShareFolder( ) } - const role = input.role?.trim() || 'viewer' + const role = input.role?.trim() || NSFShareRoleName.Viewer const accessRoleType = action === NsfFolderShareAction.Grant ? resolveNsfRoleName(role) : undefined @@ -378,7 +385,7 @@ export async function shareNestedShareFolder( expireAt: input.expireAt, expireIn: input.expireIn, expirationTimestamp: input.expirationTimestamp, - cmdName: 'nsf-share-folder', + cmdName: NsfShareCommandName.FolderShare, }) : undefined @@ -505,7 +512,7 @@ async function transferRecordOwnership( recordUid, email, success: result.success, - actionTaken: 'owner', + actionTaken: NsfRecordShareActionTaken.Owner, message: result.message, } } @@ -559,7 +566,7 @@ async function revokeRecordShare( recordUid, email, success: true, - actionTaken: 'no_access', + actionTaken: NsfRecordShareActionTaken.NoAccess, message: 'User does not have access to this record', } } @@ -568,7 +575,7 @@ async function revokeRecordShare( recordUid, email, success: true, - actionTaken: 'skipped', + actionTaken: NsfRecordShareActionTaken.Skipped, message: 'Access is inherited from a shared folder — revoke at the parent folder', } } @@ -582,7 +589,7 @@ async function revokeRecordShare( recordUid, email, success: parsed.success, - actionTaken: 'revoke', + actionTaken: NsfRecordShareActionTaken.Revoke, message: parsed.message, } } @@ -601,7 +608,7 @@ async function grantRecordShare( recordUid, email, success: true, - actionTaken: 'update', + actionTaken: NsfRecordShareActionTaken.Update, message: 'Already has requested access', } } @@ -609,7 +616,7 @@ async function grantRecordShare( if (existing && expirationTimestamp != null && expirationTimestamp > 0) { const revokeResult = await revokeRecordShare(storage, auth, recordUid, email) if (!revokeResult.success) { - return { ...revokeResult, actionTaken: 'update' } + return { ...revokeResult, actionTaken: NsfRecordShareActionTaken.Update } } const recordKey = await requireRecordKey(storage, auth, recordUid) const permission = await buildNsfRecordSharePermission( @@ -629,7 +636,7 @@ async function grantRecordShare( recordUid, email, success: parsed.success, - actionTaken: 'grant', + actionTaken: NsfRecordShareActionTaken.Grant, message: parsed.message, } } @@ -654,7 +661,7 @@ async function grantRecordShare( recordUid, email, success: parsed.success, - actionTaken: existing ? 'update' : 'grant', + actionTaken: existing ? NsfRecordShareActionTaken.Update : NsfRecordShareActionTaken.Grant, message: parsed.message, } } @@ -683,7 +690,7 @@ export function formatNsfRecordShareResults(results: NsfRecordShareResultItem[]) const lines: string[] = [] for (const item of results) { lines.push( - `${item.recordUid} ${item.email} ${item.actionTaken} ${item.success ? 'success' : 'failed'} ${item.message || ''}` + `${item.recordUid} ${item.email} ${item.actionTaken} ${item.success ? NsfResultStatus.Success : NsfResultStatus.Failed} ${item.message || ''}` ) } return lines.join('\n') @@ -694,7 +701,7 @@ export function formatNsfFolderShareResults(results: NsfFolderShareResultItem[]) const lines: string[] = [] for (const item of results) { lines.push( - `${item.folderUid} ${item.recipient} ${item.actionTaken} ${item.success ? 'success' : 'failed'} ${item.message || ''}` + `${item.folderUid} ${item.recipient} ${item.actionTaken} ${item.success ? NsfResultStatus.Success : NsfResultStatus.Failed} ${item.message || ''}` ) } return lines.join('\n') @@ -725,7 +732,7 @@ export async function shareNestedShareRecord( throw new KeeperSdkError('Role is required for grant action.', SHARE_ERROR) } - const role = input.role?.trim() || 'viewer' + const role = input.role?.trim() || NSFShareRoleName.Viewer const accessRoleType = action === NsfRecordShareAction.Grant ? resolveNsfRoleName(role) : undefined @@ -735,7 +742,7 @@ export async function shareNestedShareRecord( expireAt: input.expireAt, expireIn: input.expireIn, expirationTimestamp: input.expirationTimestamp, - cmdName: 'nsf-share-record', + cmdName: NsfShareCommandName.RecordShare, }) : undefined @@ -744,6 +751,9 @@ export async function shareNestedShareRecord( for (const recordUid of recordUids) { checkRecordSharePermission(storage, recordUid, auth.username, accountUid) + if (action === NsfRecordShareAction.Owner) { + checkRecordChangeOwnershipPermission(storage, recordUid, auth.username, accountUid) + } } const plan: NsfRecordSharePlanItem[] = [] diff --git a/KeeperSdk/src/nestedShareFolders/nsfShortcut.ts b/KeeperSdk/src/nestedShareFolders/nsfShortcut.ts index b81abfd..c285a6d 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfShortcut.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfShortcut.ts @@ -226,16 +226,16 @@ function parseFolderRecordUpdateResult( folderUid: string, recordUid: string ): KeepNsfShortcutResultItem { - const result = response.folderRecordUpdateResult?.find((entry) => { - if (!entry.recordUid?.length) return true - return webSafe64FromBytes(entry.recordUid) === recordUid - }) ?? response.folderRecordUpdateResult?.[0] + const result = response.folderRecordUpdateResult?.find( + (entry) => + !!entry.recordUid?.length && webSafe64FromBytes(entry.recordUid) === recordUid + ) if (!result) { return { folderUid, - success: true, - message: 'Record unlinked from folder', + success: false, + message: `No unlink status returned for record ${recordUid}`, } } diff --git a/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts b/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts index 974fd96..4452bb6 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts @@ -6,27 +6,17 @@ import { platform, teamGetKeysCommand, webSafe64FromBytes, - type TeamGetKeyEntry, type TeamGetKeysResponse, type Records, } from '@keeper-security/keeperapi' -import * as crypto from 'crypto' import type { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError, ResultCodes, extractErrorMessage, isValidEmail, logger } from '../utils' +import { KeeperSdkError, ResultCodes, extractErrorMessage, isValidEmail } from '../utils' import { TeamGetKeysResponseKeyType } from './nsfConstants' import type { NsfResolvedShareRecipient, NsfTeamPublicKeys } from './nsfTypes' const SHARE_ERROR = ResultCodes.NSF_SHARE_FAILED -function deriveRsaPublicKeyFromPkcs1PrivateKey(privateKeyDer: Uint8Array): Uint8Array { - const privateKey = crypto.createPrivateKey({ - key: Buffer.from(privateKeyDer), - format: 'der', - type: 'pkcs1', - }) - const publicKey = crypto.createPublicKey(privateKey) - return new Uint8Array(publicKey.export({ format: 'der', type: 'pkcs1' })) -} +type TeamGetKeyEntry = NonNullable[number] function hasAsymmetricTeamPublicKeys(keys: NsfTeamPublicKeys): boolean { return Boolean(keys.rsaPublicKey?.length || keys.eccPublicKey?.length) @@ -88,23 +78,6 @@ async function parseTeamGetKeysApiResponse( return keys } -async function fetchNsfTeamPublicKeysFromVaultStorage( - storage: InMemoryStorage, - teamUid: string -): Promise { - const teamPrivateKey = await storage.getKeyBytes(`${teamUid}_priv`) - if (!teamPrivateKey?.length) return {} - - try { - return { rsaPublicKey: deriveRsaPublicKeyFromPkcs1PrivateKey(teamPrivateKey) } - } catch (err) { - logger.debug( - `Could not derive team RSA public key from vault storage for ${teamUid}: ${extractErrorMessage(err)}` - ) - return {} - } -} - function findMatchingShareTeams( teams: Records.IShareTeam[], query: string @@ -120,7 +93,7 @@ function findMatchingShareTeams( export async function fetchNsfTeamPublicKeys( auth: Auth, teamUid: string, - storage?: InMemoryStorage + _storage?: InMemoryStorage ): Promise { const trimmed = teamUid.trim() if (!trimmed) { @@ -129,16 +102,7 @@ export async function fetchNsfTeamPublicKeys( try { const response = await auth.executeRestCommand(teamGetKeysCommand({ teams: [trimmed] })) - let keys = await parseTeamGetKeysApiResponse(auth, trimmed, response) - - if (!hasAsymmetricTeamPublicKeys(keys) && storage) { - const storageKeys = await fetchNsfTeamPublicKeysFromVaultStorage(storage, trimmed) - keys = { - rsaPublicKey: keys.rsaPublicKey ?? storageKeys.rsaPublicKey, - eccPublicKey: keys.eccPublicKey ?? storageKeys.eccPublicKey, - aesTeamKey: keys.aesTeamKey ?? storageKeys.aesTeamKey, - } - } + const keys = await parseTeamGetKeysApiResponse(auth, trimmed, response) if (!hasAsymmetricTeamPublicKeys(keys)) { throw new KeeperSdkError( diff --git a/KeeperSdk/src/nestedShareFolders/nsfTransferRecord.ts b/KeeperSdk/src/nestedShareFolders/nsfTransferRecord.ts index 1e1f703..8637764 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfTransferRecord.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfTransferRecord.ts @@ -15,6 +15,7 @@ import type { TransferNestedShareRecordResult, TransferNestedShareRecordResultItem, } from './nsfTypes' +import { NsfResultStatus, NsfTransferApiStatus } from './nsfTypes' const TRANSFER_ERROR = ResultCodes.NSF_TRANSFER_FAILED @@ -62,7 +63,8 @@ export async function transferNestedShareRecordOwnership( ) const status = response.transferRecordStatus?.[0] - const success = status?.status?.toLowerCase().includes('success') ?? false + const normalized = (status?.status ?? '').trim().toLowerCase() + const success = normalized === NsfTransferApiStatus.Success return { recordUid, success, @@ -121,7 +123,7 @@ export function formatTransferNestedShareRecordResults( const lines: string[] = [] for (const item of results) { lines.push( - `${item.recordUid} ${item.success ? 'success' : 'failed'} ${item.message}` + `${item.recordUid} ${item.success ? NsfResultStatus.Success : NsfResultStatus.Failed} ${item.message}` ) } return lines.join('\n') diff --git a/KeeperSdk/src/nestedShareFolders/nsfTypes.ts b/KeeperSdk/src/nestedShareFolders/nsfTypes.ts index 3625a78..9339553 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfTypes.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfTypes.ts @@ -399,6 +399,47 @@ export enum NsfFolderShareAction { Remove = 'remove', } +export enum NsfFolderShareActionTaken { + AlreadyHadAccess = 'already_had_access', + Updated = 'updated', + Granted = 'granted', + Removed = 'removed', +} + +export enum NsfRecordShareAction { + Grant = 'grant', + Revoke = 'revoke', + Owner = 'owner', +} + +export enum NsfRecordShareActionTaken { + Grant = 'grant', + Update = 'update', + Revoke = 'revoke', + Owner = 'owner', + NoAccess = 'no_access', + Skipped = 'skipped', +} + +export enum NsfRecordPermissionAction { + Grant = 'grant', + Revoke = 'revoke', +} + +export enum NsfResultStatus { + Success = 'success', + Failed = 'failed', +} + +/** API transferRecordStatus success value for records_transfer_v3. */ +export enum NsfTransferApiStatus { + Success = 'success', +} + +export enum NsfPermissionFailureCode { + Skipped = 'skipped', +} + export type NsfTeamPublicKeys = { rsaPublicKey?: Uint8Array eccPublicKey?: Uint8Array @@ -449,7 +490,7 @@ export type NsfFolderShareResultItem = { recipient: string isTeam: boolean success: boolean - actionTaken: string + actionTaken: NsfFolderShareActionTaken message?: string } @@ -457,12 +498,6 @@ export type ShareNestedShareFolderResult = { results: NsfFolderShareResultItem[] } -export enum NsfRecordShareAction { - Grant = 'grant', - Revoke = 'revoke', - Owner = 'owner', -} - export type NsfRecordShareActionInput = NsfRecordShareAction | `${NsfRecordShareAction}` export type ShareNestedShareRecordInput = { @@ -484,7 +519,7 @@ export type NsfRecordSharePlanItem = { recordUid: string title: string email: string - action: string + action: NsfRecordShareAction role?: string expirationTimestamp?: number } @@ -493,7 +528,7 @@ export type NsfRecordShareResultItem = { recordUid: string email: string success: boolean - actionTaken: string + actionTaken: NsfRecordShareActionTaken message?: string } @@ -503,11 +538,6 @@ export type ShareNestedShareRecordResult = { results: NsfRecordShareResultItem[] } -export enum NsfRecordPermissionAction { - Grant = 'grant', - Revoke = 'revoke', -} - export type NsfRecordPermissionActionInput = NsfRecordPermissionAction | `${NsfRecordPermissionAction}` export type UpdateNsfRecordPermissionInput = { diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 5c76f34..23bfa38 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -955,7 +955,7 @@ export class KeeperVault { input: TransferNestedShareRecordInput ): Promise { const result = await this.nestedShareFolderManager.transferNestedShareRecords(input) - if (result.success) await this.syncIfNeeded() + if (result.results.some((item) => item.success)) await this.syncIfNeeded() return result } From d1ad1a458e97e8f0aedd6f64ff0d24ac7b3ca648 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Wed, 15 Jul 2026 18:42:34 +0530 Subject: [PATCH 2/4] reverted rsa public key derivation --- .../src/nestedShareFolders/nsfConstants.ts | 1 - .../src/nestedShareFolders/nsfTeamShare.ts | 45 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/KeeperSdk/src/nestedShareFolders/nsfConstants.ts b/KeeperSdk/src/nestedShareFolders/nsfConstants.ts index 546a1b7..24c6b2d 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfConstants.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfConstants.ts @@ -192,7 +192,6 @@ type FolderRolePermissionFlags = { canListFolders?: boolean } -/** Role → FolderPermissions flags sent with folderAccessAdds / folderAccessUpdates. */ const NSF_FOLDER_ROLE_PERMISSIONS: Record = { [Folder.AccessRoleType.NAVIGATOR]: { canListFolders: true, diff --git a/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts b/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts index 4452bb6..e084c9b 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts @@ -9,8 +9,9 @@ import { type TeamGetKeysResponse, type Records, } from '@keeper-security/keeperapi' +import * as crypto from 'crypto' import type { InMemoryStorage } from '../storage/InMemoryStorage' -import { KeeperSdkError, ResultCodes, extractErrorMessage, isValidEmail } from '../utils' +import { KeeperSdkError, ResultCodes, extractErrorMessage, isValidEmail, logger } from '../utils' import { TeamGetKeysResponseKeyType } from './nsfConstants' import type { NsfResolvedShareRecipient, NsfTeamPublicKeys } from './nsfTypes' @@ -18,6 +19,18 @@ const SHARE_ERROR = ResultCodes.NSF_SHARE_FAILED type TeamGetKeyEntry = NonNullable[number] +// TODO(browser): Replace Node crypto with platform-safe PKCS#1 public-key derivation +// so team vault-storage fallback works in browser builds. +function deriveRsaPublicKeyFromPkcs1PrivateKey(privateKeyDer: Uint8Array): Uint8Array { + const privateKey = crypto.createPrivateKey({ + key: Buffer.from(privateKeyDer), + format: 'der', + type: 'pkcs1', + }) + const publicKey = crypto.createPublicKey(privateKey) + return new Uint8Array(publicKey.export({ format: 'der', type: 'pkcs1' })) +} + function hasAsymmetricTeamPublicKeys(keys: NsfTeamPublicKeys): boolean { return Boolean(keys.rsaPublicKey?.length || keys.eccPublicKey?.length) } @@ -78,6 +91,23 @@ async function parseTeamGetKeysApiResponse( return keys } +async function fetchNsfTeamPublicKeysFromVaultStorage( + storage: InMemoryStorage, + teamUid: string +): Promise { + const teamPrivateKey = await storage.getKeyBytes(`${teamUid}_priv`) + if (!teamPrivateKey?.length) return {} + + try { + return { rsaPublicKey: deriveRsaPublicKeyFromPkcs1PrivateKey(teamPrivateKey) } + } catch (err) { + logger.debug( + `Could not derive team RSA public key from vault storage for ${teamUid}: ${extractErrorMessage(err)}` + ) + return {} + } +} + function findMatchingShareTeams( teams: Records.IShareTeam[], query: string @@ -93,7 +123,7 @@ function findMatchingShareTeams( export async function fetchNsfTeamPublicKeys( auth: Auth, teamUid: string, - _storage?: InMemoryStorage + storage?: InMemoryStorage ): Promise { const trimmed = teamUid.trim() if (!trimmed) { @@ -102,7 +132,16 @@ export async function fetchNsfTeamPublicKeys( try { const response = await auth.executeRestCommand(teamGetKeysCommand({ teams: [trimmed] })) - const keys = await parseTeamGetKeysApiResponse(auth, trimmed, response) + let keys = await parseTeamGetKeysApiResponse(auth, trimmed, response) + + if (!hasAsymmetricTeamPublicKeys(keys) && storage) { + const storageKeys = await fetchNsfTeamPublicKeysFromVaultStorage(storage, trimmed) + keys = { + rsaPublicKey: keys.rsaPublicKey ?? storageKeys.rsaPublicKey, + eccPublicKey: keys.eccPublicKey ?? storageKeys.eccPublicKey, + aesTeamKey: keys.aesTeamKey ?? storageKeys.aesTeamKey, + } + } if (!hasAsymmetricTeamPublicKeys(keys)) { throw new KeeperSdkError( From 708e477ef1abb9a91c2c407376fc18e231eb014c Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Wed, 15 Jul 2026 21:40:57 +0530 Subject: [PATCH 3/4] Folder reshare logic update --- .../src/nestedShareFolders/nsfHelpers.ts | 64 +++++++++++++++++++ KeeperSdk/src/nestedShareFolders/nsfShare.ts | 10 ++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts index 0e6404d..1753dc3 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -10,6 +10,7 @@ import type { import { Folder, Records, + getFolderAccessMessage, getRecordAccessMessage, getShareObjectsMessage, normal64Bytes, @@ -1002,6 +1003,69 @@ export function findFolderAccessEntry( ) } +function toFolderAccessEntry(folderUid: string, accessor: Folder.IFolderAccessData): DKdFolderAccess { + return { + kind: 'keeper_drive_folder_access', + accessUid: `${folderUid}:${webSafe64FromBytes(accessor.accessTypeUid!)}`, + folderUid, + accessTypeUid: webSafe64FromBytes(accessor.accessTypeUid!), + accessType: accessor.accessType!, + accessRoleType: accessor.accessRoleType!, + permission: accessor.permissions ?? {}, + inherited: accessor.inherited ?? undefined, + hidden: accessor.hidden ?? undefined, + } +} + +/** Live folder accessors — sync-down often only includes the current user's own access. */ +export async function fetchLiveFolderAccessEntries( + auth: Auth, + folderUid: string +): Promise { + try { + const response = await auth.executeRest( + getFolderAccessMessage({ folderUid: [normal64Bytes(folderUid)] }) + ) + const result = response.folderAccessResults?.find( + (entry) => entry.folderUid?.length && webSafe64FromBytes(entry.folderUid) === folderUid + ) + return (result?.accessors ?? []) + .filter( + (accessor) => + accessor.accessTypeUid?.length && + accessor.accessType != null && + accessor.accessRoleType != null + ) + .map((accessor) => toFolderAccessEntry(folderUid, accessor)) + } catch (err) { + throw new KeeperSdkError( + `Failed to fetch folder permissions for ${folderUid}: ${extractErrorMessage(err)}`, + ResultCodes.NSF_DETAILS_FAILED + ) + } +} + +export async function findFolderAccessEntryOrLive( + storage: InMemoryStorage, + auth: Auth, + folderUid: string, + accessTypeUid: string, + accessType: Folder.AccessType +): Promise { + const fromStorage = findFolderAccessEntry(storage, folderUid, accessTypeUid, accessType) + if (fromStorage) return fromStorage + + try { + const liveEntries = await fetchLiveFolderAccessEntries(auth, folderUid) + return liveEntries.find( + (entry) => entry.accessTypeUid === accessTypeUid && entry.accessType === accessType + ) + } catch { + // If live lookup fails, fall through to add-path (previous behavior). + return undefined + } +} + export function collectExistingFolderShareTargets( storage: InMemoryStorage, folderUid: string, diff --git a/KeeperSdk/src/nestedShareFolders/nsfShare.ts b/KeeperSdk/src/nestedShareFolders/nsfShare.ts index 0f40dfa..8b0f7ec 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfShare.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfShare.ts @@ -31,7 +31,7 @@ import { fetchLiveRecordAccessesV3, findDirectUserRecordShare, findUserRecordShareEntry, - findFolderAccessEntry, + findFolderAccessEntryOrLive, loadShareUserMap, getKeeperDriveRecord, getNsfRecordPermissionRoleLabel, @@ -278,7 +278,13 @@ async function grantFolderAccess( const accessType = target.isTeam ? Folder.AccessType.AT_TEAM : Folder.AccessType.AT_USER const accessTypeUid = target.accountUid ? webSafe64FromBytes(target.accountUid) : target.recipient - const existing = findFolderAccessEntry(storage, folderUid, accessTypeUid, accessType) + const existing = await findFolderAccessEntryOrLive( + storage, + auth, + folderUid, + accessTypeUid, + accessType + ) if (existing && existing.accessRoleType === accessRoleType && expirationTimestamp == null) { return { folderUid, From 4a1af00c04169594b6c13d688748675c6a3953b1 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Wed, 15 Jul 2026 22:11:08 +0530 Subject: [PATCH 4/4] one liner change --- KeeperSdk/src/nestedShareFolders/nsfHelpers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts index 1753dc3..abd3510 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -1017,7 +1017,6 @@ function toFolderAccessEntry(folderUid: string, accessor: Folder.IFolderAccessDa } } -/** Live folder accessors — sync-down often only includes the current user's own access. */ export async function fetchLiveFolderAccessEntries( auth: Auth, folderUid: string @@ -1061,7 +1060,6 @@ export async function findFolderAccessEntryOrLive( (entry) => entry.accessTypeUid === accessTypeUid && entry.accessType === accessType ) } catch { - // If live lookup fails, fall through to add-path (previous behavior). return undefined } }