Skip to content
Open
45 changes: 45 additions & 0 deletions KeeperSdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,30 @@ export {
formatNsfFolderDetail,
formatNsfRecordDetail,
formatNsfDetail,
formatNsfJson,
formatNsfRecordJson,
toNsfRecordJsonView,
linkNestedShareRecord,
NsfRemoveOperation,
removeNestedShareRecords,
formatRemoveNsfPreview,
collectRemoveNsfWarnings,
mkdirNestedShareFolder,
NSF_FOLDER_COLORS,
NsfRemoveFolderOperation,
removeNestedShareFolders,
formatRemoveNsfFolderPreview,
GetNsfRecordDetailsFormat,
getNestedShareRecordDetails,
formatNsfRecordDetailsTable,
formatNsfRecordDetailsOutput,
updateNestedShareRecords,
addNestedShareRecord,
buildNsfRecordData,
parseNsfFieldInput,
parseNsfFieldSpaceInput,
resolveNsfFieldValue,
checkRecordEditPermission,
NestedShareFolderManager,
} from './nestedShareFolders'
export type {
Expand All @@ -487,6 +507,10 @@ export type {
GetNsfResult,
NsfFolderView,
NsfRecordView,
NsfRecordFieldView,
NsfRecordFolderView,
NsfRecordJsonView,
NsfRecordJsonUserPermission,
NsfFolderPermission,
NsfFolderAccessRow,
NsfRecordPermission,
Expand All @@ -495,6 +519,27 @@ export type {
RemoveNsfRecordInput,
NsfRemovePreviewItem,
RemoveNsfRecordResult,
MkdirNsfInput,
MkdirNsfResult,
NsfFolderColorInput,
NsfFolderColor,
NsfRemoveFolderOperationInput,
RemoveNsfFolderInput,
NsfRemoveFolderPreviewItem,
RemoveNsfFolderResult,
GetNsfRecordDetailsFormatInput,
GetNsfRecordDetailsInput,
GetNsfRecordDetailsResult,
NsfRecordDetailsItem,
UpdateNsfRecordInput,
UpdateNsfRecordResult,
UpdateNsfRecordResultItem,
UpdateNsfRecordFieldEntry,
AddNsfRecordInput,
AddNsfRecordResult,
ParsedNsfFields,
ParsedNsfFieldStrings,
RecordFieldEntry,
} from './nestedShareFolders'

export type {
Expand Down
61 changes: 61 additions & 0 deletions KeeperSdk/src/nestedShareFolders/NestedShareFolderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './listNsf'
import {
formatNsfDetail as renderNsfDetail,
formatNsfJson as renderNsfJson,
formatNsfFolderDetail as renderNsfFolderDetail,
formatNsfRecordDetail as renderNsfRecordDetail,
getNestedShareFolder,
Expand All @@ -28,6 +29,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 @@ -72,6 +92,10 @@ export class NestedShareFolderManager {
return renderNsfDetail(result, verbose)
}

public formatNsfJson(result: GetNsfResult): string {
return renderNsfJson(result)
}

public formatNsfFolderDetail(view: NsfFolderView, verbose = false): string {
return renderNsfFolderDetail(view, verbose)
}
Expand All @@ -94,4 +118,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)
}
}
163 changes: 163 additions & 0 deletions KeeperSdk/src/nestedShareFolders/addNsfRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import type { Auth, record as RecordProto } from '@keeper-security/keeperapi'
import {
Folder,
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 RecordFieldEntry,
} from './nsfRecordData'
import {
ensureNestedShareFolder,
nsfToNumber,
parseRecordModifyStatus,
requireAuthDataKey,
resolveNsfFolderIdentifier,
} from './nsfHelpers'
import { validateNsfRecordType } from './nsfRecordTypes'

export type { RecordFieldEntry } from './nsfRecordData'

export type AddNsfRecordInput = {
title: string
recordType: string
folder?: string
notes?: string
fieldEntries?: RecordFieldEntry[]
customEntries?: RecordFieldEntry[]
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
)
}

if (!input.recordData && input.recordType?.trim()) {
await validateNsfRecordType(auth, input.recordType, ResultCodes.NSF_ADD_FAILED)
}

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

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
)
}
}
Loading
Loading