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..24c6b2d 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 { @@ -236,7 +254,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..abd3510 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, @@ -36,6 +37,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 +488,7 @@ type FolderPermissionFlag = | 'canDelete' | 'canUpdateAccess' | 'canEditRecords' + | 'canUpdateSetting' | 'canChangeOwnership' function hasFolderPermission( @@ -807,7 +811,6 @@ export function resolveAccessUsername( return accessTypeUid } - export async function loadShareUserMap(auth: Auth, storage: InMemoryStorage): Promise> { const map = new Map() @@ -837,7 +840,6 @@ export async function loadShareUserMap(auth: Auth, storage: InMemoryStorage): Pr return map } - export function isFolderOwnerAccessor( folder: DKdFolder, entry: DKdFolderAccess, @@ -944,7 +946,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 @@ -1001,6 +1003,67 @@ 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, + } +} + +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 { + return undefined + } +} + export function collectExistingFolderShareTargets( storage: InMemoryStorage, folderUid: string, @@ -1043,7 +1106,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 +1274,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 +1300,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 +1340,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..8b0f7ec 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, @@ -30,7 +31,7 @@ import { fetchLiveRecordAccessesV3, findDirectUserRecordShare, findUserRecordShareEntry, - findFolderAccessEntry, + findFolderAccessEntryOrLive, loadShareUserMap, getKeeperDriveRecord, getNsfRecordPermissionRoleLabel, @@ -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 @@ -271,14 +278,20 @@ 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, recipient: target.recipient, isTeam: target.isTeam, success: true, - actionTaken: 'already_had_access', + actionTaken: NsfFolderShareActionTaken.AlreadyHadAccess, message: `Already has ${getNsfRecordPermissionRoleLabel(accessRoleType)} access`, } } @@ -311,7 +324,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 +357,7 @@ async function removeFolderAccess( recipient: target.recipient, isTeam: target.isTeam, success: parsed.success, - actionTaken: 'removed', + actionTaken: NsfFolderShareActionTaken.Removed, message: parsed.message, } } @@ -368,7 +381,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 +391,7 @@ export async function shareNestedShareFolder( expireAt: input.expireAt, expireIn: input.expireIn, expirationTimestamp: input.expirationTimestamp, - cmdName: 'nsf-share-folder', + cmdName: NsfShareCommandName.FolderShare, }) : undefined @@ -505,7 +518,7 @@ async function transferRecordOwnership( recordUid, email, success: result.success, - actionTaken: 'owner', + actionTaken: NsfRecordShareActionTaken.Owner, message: result.message, } } @@ -559,7 +572,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 +581,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 +595,7 @@ async function revokeRecordShare( recordUid, email, success: parsed.success, - actionTaken: 'revoke', + actionTaken: NsfRecordShareActionTaken.Revoke, message: parsed.message, } } @@ -601,7 +614,7 @@ async function grantRecordShare( recordUid, email, success: true, - actionTaken: 'update', + actionTaken: NsfRecordShareActionTaken.Update, message: 'Already has requested access', } } @@ -609,7 +622,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 +642,7 @@ async function grantRecordShare( recordUid, email, success: parsed.success, - actionTaken: 'grant', + actionTaken: NsfRecordShareActionTaken.Grant, message: parsed.message, } } @@ -654,7 +667,7 @@ async function grantRecordShare( recordUid, email, success: parsed.success, - actionTaken: existing ? 'update' : 'grant', + actionTaken: existing ? NsfRecordShareActionTaken.Update : NsfRecordShareActionTaken.Grant, message: parsed.message, } } @@ -683,7 +696,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 +707,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 +738,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 +748,7 @@ export async function shareNestedShareRecord( expireAt: input.expireAt, expireIn: input.expireIn, expirationTimestamp: input.expirationTimestamp, - cmdName: 'nsf-share-record', + cmdName: NsfShareCommandName.RecordShare, }) : undefined @@ -744,6 +757,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..e084c9b 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfTeamShare.ts @@ -6,7 +6,6 @@ import { platform, teamGetKeysCommand, webSafe64FromBytes, - type TeamGetKeyEntry, type TeamGetKeysResponse, type Records, } from '@keeper-security/keeperapi' @@ -18,6 +17,10 @@ import type { NsfResolvedShareRecipient, NsfTeamPublicKeys } from './nsfTypes' 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), 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 }