Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion KeeperSdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,13 +445,13 @@ export {
export { UserManager } from './users/UserManager'

export {
ROOT_FOLDER_UID,
KeeperDriveKind,
NsfItemType,
formatAccessRoleType,
formatAccessType,
normalizeParentUid,
isRootFolderUid,
resolveKeeperDriveRootParentUid,
getKeeperDriveFolders,
getKeeperDriveRecords,
findRecordFolderLocation,
Expand All @@ -475,6 +475,20 @@ export {
NsfRemoveOperation,
removeNestedShareRecords,
formatRemoveNsfPreview,
mkdirNestedShareFolder,
NSF_FOLDER_COLORS,
NsfRemoveFolderOperation,
removeNestedShareFolders,
formatRemoveNsfFolderPreview,
GetNsfRecordDetailsFormat,
getNestedShareRecordDetails,
formatNsfRecordDetailsTable,
formatNsfRecordDetailsOutput,
updateNestedShareRecords,
addNestedShareRecord,
buildNsfRecordData,
parseNsfFieldStrings,
checkRecordEditPermission,
NestedShareFolderManager,
} from './nestedShareFolders'
export type {
Expand All @@ -495,6 +509,27 @@ export type {
RemoveNsfRecordInput,
NsfRemovePreviewItem,
RemoveNsfRecordResult,
MkdirNsfInput,
MkdirNsfResult,
NsfFolderColorInput,
NsfFolderColor,
NsfRemoveFolderOperationInput,
RemoveNsfFolderInput,
NsfRemoveFolderPreviewItem,
RemoveNsfFolderResult,
GetNsfRecordDetailsFormatInput,
GetNsfRecordDetailsInput,
GetNsfRecordDetailsResult,
NsfRecordDetailsItem,
UpdateNsfRecordInput,
UpdateNsfRecordResult,
UpdateNsfRecordResultItem,
UpdateNsfRecordFieldMap,
AddNsfRecordInput,
AddNsfRecordResult,
NsfRecordFieldMap,
NsfRecordCustomField,
ParsedNsfFieldStrings,
} from './nestedShareFolders'

export type {
Expand Down
56 changes: 56 additions & 0 deletions KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ import {
type RemoveNsfRecordInput,
type RemoveNsfRecordResult,
} from './removeNsfRecord'
import { mkdirNestedShareFolder, type MkdirNsfInput, type MkdirNsfResult } from './mkdirNsf'
import {
formatRemoveNsfFolderPreview,
removeNestedShareFolders,
type RemoveNsfFolderInput,
type RemoveNsfFolderResult,
} from './removeNsfFolder'
import {
getNestedShareRecordDetails,
formatNsfRecordDetailsOutput,
type GetNsfRecordDetailsInput,
type GetNsfRecordDetailsResult,
} from './getNsfRecordDetails'
import {
updateNestedShareRecords,
type UpdateNsfRecordInput,
type UpdateNsfRecordResult,
} from './updateNsfRecord'
import { addNestedShareRecord, type AddNsfRecordInput, type AddNsfRecordResult } from './addNsfRecord'

export type AuthProvider = () => Auth

Expand Down Expand Up @@ -94,4 +113,41 @@ export class NestedShareFolderManager {
public formatRemoveNsfPreview(preview: RemoveNsfRecordResult['preview']): string {
return formatRemoveNsfPreview(preview)
}

public async mkdirNestedShareFolder(input: MkdirNsfInput): Promise<MkdirNsfResult> {
return mkdirNestedShareFolder(this.storage, this.requireAuth(), input)
}

public async removeNestedShareFolders(input: RemoveNsfFolderInput): Promise<RemoveNsfFolderResult> {
return removeNestedShareFolders(this.storage, this.requireAuth(), input)
}

public formatRemoveNsfFolderPreview(
preview: RemoveNsfFolderResult['preview'],
operation: RemoveNsfFolderResult['operation'],
quiet?: boolean
): string {
return formatRemoveNsfFolderPreview(preview, operation, quiet)
}

public async getNestedShareRecordDetails(
input: GetNsfRecordDetailsInput
): Promise<GetNsfRecordDetailsResult> {
return getNestedShareRecordDetails(this.storage, this.requireAuth(), input)
}

public formatNsfRecordDetailsOutput(
result: GetNsfRecordDetailsResult,
format?: GetNsfRecordDetailsInput['format']
): string {
return formatNsfRecordDetailsOutput(result, format)
}

public async updateNestedShareRecords(input: UpdateNsfRecordInput): Promise<UpdateNsfRecordResult> {
return updateNestedShareRecords(this.storage, this.requireAuth(), input)
}

public async addNestedShareRecord(input: AddNsfRecordInput): Promise<AddNsfRecordResult> {
return addNestedShareRecord(this.storage, this.requireAuth(), input)
}
}
160 changes: 160 additions & 0 deletions KeeperSdk/src/nestedShareFolders/addNsfRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { Auth, record as RecordProto } from '@keeper-security/keeperapi'
import {
Folder,
Records,
generateEncryptionKey,
generateUid,
keeperDriveRecordsAdd,
normal64Bytes,
platform,
} from '@keeper-security/keeperapi'
import type { InMemoryStorage } from '../storage/InMemoryStorage'
import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../utils'
import {
buildNsfRecordData,
getPaddedJsonBytes,
type NsfRecordCustomField,
type NsfRecordFieldMap,
} from './nsfRecordData'
import {
ensureNestedShareFolder,
nsfToNumber,
parseRecordModifyStatus,
requireAuthDataKey,
resolveNsfFolderIdentifier,
} from './nsfHelpers'

export type { NsfRecordFieldMap, NsfRecordCustomField } from './nsfRecordData'

export type AddNsfRecordInput = {
title: string
recordType: string
folder?: string
notes?: string
fields?: NsfRecordFieldMap
custom?: NsfRecordCustomField[]
recordData?: Record<string, unknown>
force?: boolean
hasFileFields?: boolean
}

export type AddNsfRecordResult = {
recordUid: string
success: boolean
status: string
message?: string
revision?: number
}

function resolveFolderUid(storage: InMemoryStorage, folderInput?: string): string | undefined {
const trimmed = folderInput?.trim()
if (!trimmed) return undefined

const folderUid = resolveNsfFolderIdentifier(storage, trimmed)
if (!folderUid) {
throw new KeeperSdkError(`No such folder: ${trimmed}`, ResultCodes.NSF_NOT_FOUND)
}
ensureNestedShareFolder(storage, folderUid, trimmed)
return folderUid
}

async function requireFolderKey(storage: InMemoryStorage, folderUid: string): Promise<Uint8Array> {
const folderKey = await storage.getKeyBytes(folderUid)
if (folderKey) return folderKey
throw new KeeperSdkError(
`Folder key not found for ${folderUid}. Run sync() first.`,
ResultCodes.NSF_MISSING_KEY
)
}

async function buildRecordAdd(
storage: InMemoryStorage,
auth: Auth,
recordData: Record<string, unknown>,
folderUid?: string
): Promise<{ recordUid: string; recordAdd: RecordProto.v3.IRecordAdd }> {
const recordUid = generateUid()
const recordKey = generateEncryptionKey()
await storage.saveKeyBytes(recordUid, recordKey)

const recordAdd: RecordProto.v3.IRecordAdd = {
recordUid: normal64Bytes(recordUid),
clientModifiedTime: Date.now(),
data: await platform.aesGcmEncrypt(getPaddedJsonBytes(recordData), recordKey),
}

if (folderUid) {
const folderKey = await requireFolderKey(storage, folderUid)
recordAdd.folderUid = normal64Bytes(folderUid)
recordAdd.recordKey = await platform.aesGcmEncrypt(recordKey, folderKey)
recordAdd.recordKeyEncryptedBy = Folder.FolderKeyEncryptionType.ENCRYPTED_BY_PARENT_KEY
recordAdd.recordKeyType = Folder.EncryptedKeyType.encrypted_by_data_key_gcm
} else {
recordAdd.recordKey = await platform.aesGcmEncrypt(recordKey, requireAuthDataKey(auth))
recordAdd.recordKeyType = Folder.EncryptedKeyType.encrypted_by_data_key_gcm
}

return { recordUid, recordAdd }
}

export async function addNestedShareRecord(
storage: InMemoryStorage,
auth: Auth,
input: AddNsfRecordInput
): Promise<AddNsfRecordResult> {
if (!input.title?.trim()) {
throw new KeeperSdkError('Record title is required.', ResultCodes.NSF_ADD_FAILED)
}
if (!input.recordType?.trim() && !input.recordData) {
throw new KeeperSdkError('Record type is required.', ResultCodes.NSF_ADD_FAILED)
}
if (input.hasFileFields && !input.force) {
throw new KeeperSdkError(
'File attachments are not supported in nested share record add.',
ResultCodes.NSF_ADD_FAILED
)
}

const recordData =
input.recordData ??
buildNsfRecordData({
title: input.title,
recordType: input.recordType,
notes: input.notes,
fields: input.fields,
custom: input.custom,
})

try {
const { recordUid, recordAdd } = await buildRecordAdd(
storage,
auth,
recordData,
resolveFolderUid(storage, input.folder)
)
const response = await auth.executeRest(
keeperDriveRecordsAdd({
records: [recordAdd],
clientTime: Date.now(),
})
)
const { statusName, message } = parseRecordModifyStatus(
response.records?.[0],
ResultCodes.NSF_ADD_FAILED
)

return {
recordUid,
success: true,
status: statusName,
message,
revision: nsfToNumber(response.revision),
}
} catch (err) {
if (err instanceof KeeperSdkError) throw err
throw new KeeperSdkError(
`Failed to add nested share record: ${extractErrorMessage(err)}`,
ResultCodes.NSF_ADD_FAILED
)
}
}
2 changes: 1 addition & 1 deletion KeeperSdk/src/nestedShareFolders/getNsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ function buildFolderView(storage: InMemoryStorage, folderUid: string): NsfFolder
objectType: 'folder',
folderUid,
name: folder.data.name || 'Unnamed',
parentUid: normalizeParentUid(folder.parentUid),
parentUid: normalizeParentUid(storage, folder.parentUid),
path: buildFolderPath(storage, folderUid),
userPermissions,
shareAdmins,
Expand Down
Loading
Loading