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
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