@keeper-security/keeper-sdk-javascript 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/auth/ConsoleAuthUI.d.ts +10 -0
  2. package/dist/auth/ConsoleAuthUI.js +152 -0
  3. package/dist/auth/ConsoleAuthUI.js.map +1 -0
  4. package/dist/auth/ConsoleLogin.d.ts +8 -0
  5. package/dist/auth/ConsoleLogin.js +266 -0
  6. package/dist/auth/ConsoleLogin.js.map +1 -0
  7. package/dist/auth/SessionManager.d.ts +66 -0
  8. package/dist/auth/SessionManager.js +211 -0
  9. package/dist/auth/SessionManager.js.map +1 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.js +61 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/records/RecordOperations.d.ts +79 -0
  14. package/dist/records/RecordOperations.js +346 -0
  15. package/dist/records/RecordOperations.js.map +1 -0
  16. package/dist/records/RecordUtils.d.ts +36 -0
  17. package/dist/records/RecordUtils.js +224 -0
  18. package/dist/records/RecordUtils.js.map +1 -0
  19. package/dist/sharing/Sharing.d.ts +27 -0
  20. package/dist/sharing/Sharing.js +125 -0
  21. package/dist/sharing/Sharing.js.map +1 -0
  22. package/dist/src/auth/ConsoleAuthUI.d.ts +10 -0
  23. package/dist/src/auth/ConsoleAuthUI.js +161 -0
  24. package/dist/src/auth/ConsoleAuthUI.js.map +1 -0
  25. package/dist/src/auth/ConsoleLogin.d.ts +8 -0
  26. package/dist/src/auth/ConsoleLogin.js +311 -0
  27. package/dist/src/auth/ConsoleLogin.js.map +1 -0
  28. package/dist/src/auth/SessionManager.d.ts +67 -0
  29. package/dist/src/auth/SessionManager.js +212 -0
  30. package/dist/src/auth/SessionManager.js.map +1 -0
  31. package/dist/src/folders/FolderManager.d.ts +57 -0
  32. package/dist/src/folders/FolderManager.js +108 -0
  33. package/dist/src/folders/FolderManager.js.map +1 -0
  34. package/dist/src/folders/addFolder.d.ts +32 -0
  35. package/dist/src/folders/addFolder.js +207 -0
  36. package/dist/src/folders/addFolder.js.map +1 -0
  37. package/dist/src/folders/changeDirectory.d.ts +19 -0
  38. package/dist/src/folders/changeDirectory.js +171 -0
  39. package/dist/src/folders/changeDirectory.js.map +1 -0
  40. package/dist/src/folders/deleteFolder.d.ts +17 -0
  41. package/dist/src/folders/deleteFolder.js +237 -0
  42. package/dist/src/folders/deleteFolder.js.map +1 -0
  43. package/dist/src/folders/folderHelpers.d.ts +48 -0
  44. package/dist/src/folders/folderHelpers.js +100 -0
  45. package/dist/src/folders/folderHelpers.js.map +1 -0
  46. package/dist/src/folders/folderTree.d.ts +29 -0
  47. package/dist/src/folders/folderTree.js +250 -0
  48. package/dist/src/folders/folderTree.js.map +1 -0
  49. package/dist/src/folders/getFolder.d.ts +56 -0
  50. package/dist/src/folders/getFolder.js +143 -0
  51. package/dist/src/folders/getFolder.js.map +1 -0
  52. package/dist/src/folders/listFolder.d.ts +48 -0
  53. package/dist/src/folders/listFolder.js +276 -0
  54. package/dist/src/folders/listFolder.js.map +1 -0
  55. package/dist/src/folders/updateFolder.d.ts +31 -0
  56. package/dist/src/folders/updateFolder.js +137 -0
  57. package/dist/src/folders/updateFolder.js.map +1 -0
  58. package/dist/src/index.d.ts +49 -0
  59. package/dist/src/index.js +151 -0
  60. package/dist/src/index.js.map +1 -0
  61. package/dist/src/records/RecordOperations.d.ts +80 -0
  62. package/dist/src/records/RecordOperations.js +356 -0
  63. package/dist/src/records/RecordOperations.js.map +1 -0
  64. package/dist/src/records/RecordUtils.d.ts +37 -0
  65. package/dist/src/records/RecordUtils.js +263 -0
  66. package/dist/src/records/RecordUtils.js.map +1 -0
  67. package/dist/src/records/Totp.d.ts +14 -0
  68. package/dist/src/records/Totp.js +111 -0
  69. package/dist/src/records/Totp.js.map +1 -0
  70. package/dist/src/sharedFolders/SharedFolderManager.d.ts +20 -0
  71. package/dist/src/sharedFolders/SharedFolderManager.js +33 -0
  72. package/dist/src/sharedFolders/SharedFolderManager.js.map +1 -0
  73. package/dist/src/sharedFolders/listSharedFolders.d.ts +29 -0
  74. package/dist/src/sharedFolders/listSharedFolders.js +127 -0
  75. package/dist/src/sharedFolders/listSharedFolders.js.map +1 -0
  76. package/dist/src/sharedFolders/shareFolder.d.ts +36 -0
  77. package/dist/src/sharedFolders/shareFolder.js +352 -0
  78. package/dist/src/sharedFolders/shareFolder.js.map +1 -0
  79. package/dist/src/sharing/Sharing.d.ts +50 -0
  80. package/dist/src/sharing/Sharing.js +195 -0
  81. package/dist/src/sharing/Sharing.js.map +1 -0
  82. package/dist/src/storage/InMemoryStorage.d.ts +24 -0
  83. package/dist/src/storage/InMemoryStorage.js +139 -0
  84. package/dist/src/storage/InMemoryStorage.js.map +1 -0
  85. package/dist/src/teams/TeamManager.d.ts +17 -0
  86. package/dist/src/teams/TeamManager.js +38 -0
  87. package/dist/src/teams/TeamManager.js.map +1 -0
  88. package/dist/src/teams/enterpriseData.d.ts +106 -0
  89. package/dist/src/teams/enterpriseData.js +319 -0
  90. package/dist/src/teams/enterpriseData.js.map +1 -0
  91. package/dist/src/teams/listTeams.d.ts +42 -0
  92. package/dist/src/teams/listTeams.js +308 -0
  93. package/dist/src/teams/listTeams.js.map +1 -0
  94. package/dist/src/teams/viewTeam.d.ts +35 -0
  95. package/dist/src/teams/viewTeam.js +177 -0
  96. package/dist/src/teams/viewTeam.js.map +1 -0
  97. package/dist/src/utils/Logger.d.ts +28 -0
  98. package/dist/src/utils/Logger.js +62 -0
  99. package/dist/src/utils/Logger.js.map +1 -0
  100. package/dist/src/utils/constants.d.ts +50 -0
  101. package/dist/src/utils/constants.js +64 -0
  102. package/dist/src/utils/constants.js.map +1 -0
  103. package/dist/src/utils/errors.d.ts +10 -0
  104. package/dist/src/utils/errors.js +117 -0
  105. package/dist/src/utils/errors.js.map +1 -0
  106. package/dist/src/utils/guards.d.ts +7 -0
  107. package/dist/src/utils/guards.js +29 -0
  108. package/dist/src/utils/guards.js.map +1 -0
  109. package/dist/src/utils/index.d.ts +7 -0
  110. package/dist/src/utils/index.js +39 -0
  111. package/dist/src/utils/index.js.map +1 -0
  112. package/dist/src/utils/patterns.d.ts +9 -0
  113. package/dist/src/utils/patterns.js +20 -0
  114. package/dist/src/utils/patterns.js.map +1 -0
  115. package/dist/src/utils/types.d.ts +12 -0
  116. package/dist/src/utils/types.js +3 -0
  117. package/dist/src/utils/types.js.map +1 -0
  118. package/dist/src/vault/KeeperVault.d.ts +116 -0
  119. package/dist/src/vault/KeeperVault.js +443 -0
  120. package/dist/src/vault/KeeperVault.js.map +1 -0
  121. package/dist/storage/InMemoryStorage.d.ts +24 -0
  122. package/dist/storage/InMemoryStorage.js +132 -0
  123. package/dist/storage/InMemoryStorage.js.map +1 -0
  124. package/dist/utils/Logger.d.ts +28 -0
  125. package/dist/utils/Logger.js +62 -0
  126. package/dist/utils/Logger.js.map +1 -0
  127. package/dist/utils/constants.d.ts +26 -0
  128. package/dist/utils/constants.js +37 -0
  129. package/dist/utils/constants.js.map +1 -0
  130. package/dist/utils/errors.d.ts +10 -0
  131. package/dist/utils/errors.js +117 -0
  132. package/dist/utils/errors.js.map +1 -0
  133. package/dist/utils/index.d.ts +4 -0
  134. package/dist/utils/index.js +22 -0
  135. package/dist/utils/index.js.map +1 -0
  136. package/dist/vault/KeeperVault.d.ts +72 -0
  137. package/dist/vault/KeeperVault.js +338 -0
  138. package/dist/vault/KeeperVault.js.map +1 -0
  139. package/package.json +32 -0
  140. package/src/auth/ConsoleAuthUI.ts +169 -0
  141. package/src/auth/ConsoleLogin.ts +351 -0
  142. package/src/auth/SessionManager.ts +293 -0
  143. package/src/folders/FolderManager.ts +174 -0
  144. package/src/folders/addFolder.ts +294 -0
  145. package/src/folders/changeDirectory.ts +217 -0
  146. package/src/folders/deleteFolder.ts +293 -0
  147. package/src/folders/folderHelpers.ts +99 -0
  148. package/src/folders/folderTree.ts +321 -0
  149. package/src/folders/getFolder.ts +234 -0
  150. package/src/folders/listFolder.ts +358 -0
  151. package/src/folders/updateFolder.ts +210 -0
  152. package/src/index.ts +242 -0
  153. package/src/records/RecordOperations.ts +549 -0
  154. package/src/records/RecordUtils.ts +282 -0
  155. package/src/records/Totp.ts +119 -0
  156. package/src/sharedFolders/SharedFolderManager.ts +57 -0
  157. package/src/sharedFolders/listSharedFolders.ts +173 -0
  158. package/src/sharedFolders/shareFolder.ts +457 -0
  159. package/src/sharing/Sharing.ts +282 -0
  160. package/src/storage/InMemoryStorage.ts +163 -0
  161. package/src/teams/TeamManager.ts +61 -0
  162. package/src/teams/enterpriseData.ts +453 -0
  163. package/src/teams/listTeams.ts +373 -0
  164. package/src/teams/viewTeam.ts +248 -0
  165. package/src/utils/Logger.ts +71 -0
  166. package/src/utils/constants.ts +63 -0
  167. package/src/utils/errors.ts +108 -0
  168. package/src/utils/guards.ts +24 -0
  169. package/src/utils/index.ts +22 -0
  170. package/src/utils/patterns.ts +20 -0
  171. package/src/utils/types.ts +11 -0
  172. package/src/vault/KeeperVault.ts +612 -0
  173. package/tsconfig.json +16 -0
@@ -0,0 +1,549 @@
1
+ import {
2
+ Auth,
3
+ Records,
4
+ platform,
5
+ generateEncryptionKey,
6
+ generateUidBytes,
7
+ webSafe64FromBytes,
8
+ recordsAddMessage,
9
+ recordsUpdateMessage,
10
+ recordPreDeleteCommand,
11
+ recordDeleteCommand,
12
+ recordAddCommand,
13
+ moveCommand,
14
+ getRecordHistoryCommand,
15
+ normal64Bytes,
16
+ } from '@keeper-security/keeperapi'
17
+ import type {
18
+ DSharedFolder,
19
+ DSharedFolderFolder,
20
+ DSharedFolderRecord,
21
+ DUserFolder,
22
+ DRecord,
23
+ KeeperPreDeleteResponse,
24
+ MoveObject,
25
+ MoveRequest,
26
+ TransitionKeyObject,
27
+ RecordPreDeleteObject,
28
+ GetRecordHistoryResponse,
29
+ } from '@keeper-security/keeperapi'
30
+ import { extractErrorMessage, KeeperSdkError, logger } from '../utils'
31
+ import { RecordVersion } from './RecordUtils'
32
+ import { InMemoryStorage } from '../storage/InMemoryStorage'
33
+ import { DeleteResolution, FolderKind, VaultObjectKind } from '../folders/folderHelpers'
34
+
35
+ enum ResultCode {
36
+ Success = 'success',
37
+ OK = 'OK',
38
+ }
39
+
40
+ const MIN_RECORD_PAD_BYTES = 384
41
+ const PAD_BLOCK_SIZE = 16
42
+
43
+ function getPaddedJsonBytes(data: Record<string, any>): Uint8Array {
44
+ const json = JSON.stringify(data)
45
+ const paddedLength = Math.ceil(Math.max(MIN_RECORD_PAD_BYTES, json.length) / PAD_BLOCK_SIZE) * PAD_BLOCK_SIZE
46
+ const padded = json.padEnd(paddedLength, ' ')
47
+ return new TextEncoder().encode(padded)
48
+ }
49
+
50
+ export type PasswordRecordData = {
51
+ title: string
52
+ login?: string
53
+ password?: string
54
+ url?: string
55
+ notes?: string
56
+ custom?: { name: string; value: string; type?: string }[]
57
+ totp?: string
58
+ }
59
+
60
+ function buildLegacyPasswordExtra(data: PasswordRecordData, recordUid: string): Record<string, unknown> {
61
+ const totp = data.totp?.trim()
62
+ if (!totp) {
63
+ return {}
64
+ }
65
+ return {
66
+ files: [],
67
+ fields: [
68
+ {
69
+ id: recordUid,
70
+ field_type: 'totp',
71
+ field_title: 'One-Time Password',
72
+ data: totp,
73
+ },
74
+ ],
75
+ }
76
+ }
77
+
78
+ export type RecordFieldInput = {
79
+ type: string
80
+ value: any[]
81
+ label?: string
82
+ }
83
+
84
+ export type TypedRecordData = {
85
+ type: string
86
+ title: string
87
+ fields?: RecordFieldInput[]
88
+ custom?: RecordFieldInput[]
89
+ notes?: string
90
+ }
91
+
92
+ export type NewRecordInput =
93
+ | { version: 2; data: PasswordRecordData; folderUid?: string }
94
+ | { version: 3; data: TypedRecordData; folderUid?: string }
95
+
96
+ export type AddRecordResult = {
97
+ recordUid: string
98
+ success: boolean
99
+ status?: string
100
+ }
101
+
102
+ export type UpdateRecordResult = {
103
+ recordUid: string
104
+ success: boolean
105
+ status?: string
106
+ }
107
+
108
+ export type DeleteRecordResult = {
109
+ recordUid: string
110
+ success: boolean
111
+ message?: string
112
+ }
113
+
114
+ export async function addRecord(
115
+ auth: Auth,
116
+ input: NewRecordInput
117
+ ): Promise<AddRecordResult> {
118
+ if (!input.data.title || !input.data.title.trim()) {
119
+ throw new KeeperSdkError('Record title is required.', 'missing_record_title')
120
+ }
121
+ if (input.version === 3 && !input.data.type?.trim()) {
122
+ throw new KeeperSdkError('Record type is required for v3 records.', 'missing_record_type')
123
+ }
124
+ if (input.version === 2) {
125
+ return addPasswordRecord(auth, input.data, input.folderUid)
126
+ }
127
+ return addTypedRecord(auth, input.data, input.folderUid)
128
+ }
129
+
130
+ async function addPasswordRecord(
131
+ auth: Auth,
132
+ data: PasswordRecordData,
133
+ folderUid?: string
134
+ ): Promise<AddRecordResult> {
135
+ const recordUidBytes = generateUidBytes()
136
+ const recordKey = generateEncryptionKey()
137
+ const recordUid = webSafe64FromBytes(recordUidBytes)
138
+
139
+ const recordDataJson = JSON.stringify({
140
+ title: data.title || '',
141
+ secret1: data.login || '',
142
+ secret2: data.password || '',
143
+ link: data.url || '',
144
+ notes: data.notes || '',
145
+ custom: (data.custom || []).map((c) => ({
146
+ name: c.name,
147
+ value: c.value,
148
+ type: c.type || 'text',
149
+ })),
150
+ })
151
+
152
+ const extraJson = JSON.stringify(buildLegacyPasswordExtra(data, recordUid))
153
+
154
+ const dataBytes = new TextEncoder().encode(recordDataJson)
155
+ const extraBytes = new TextEncoder().encode(extraJson)
156
+
157
+ const encryptedData = await platform.aesCbcEncrypt(dataBytes, recordKey, true)
158
+ const encryptedExtra = await platform.aesCbcEncrypt(extraBytes, recordKey, true)
159
+ const encryptedRecordKey = await platform.aesCbcEncrypt(recordKey, auth.dataKey!, true)
160
+
161
+ const cmd = recordAddCommand({
162
+ record_uid: recordUid,
163
+ record_key: webSafe64FromBytes(encryptedRecordKey),
164
+ record_type: 'password',
165
+ folder_type: FolderKind.UserFolder,
166
+ how_long_ago: 0,
167
+ folder_uid: folderUid || '',
168
+ folder_key: '',
169
+ data: webSafe64FromBytes(encryptedData),
170
+ extra: webSafe64FromBytes(encryptedExtra),
171
+ non_shared_data: '',
172
+ file_ids: [],
173
+ })
174
+
175
+ const response = await auth.executeRestCommand(cmd)
176
+
177
+ return {
178
+ recordUid,
179
+ success: response.result_code === ResultCode.Success,
180
+ status: response.result_code,
181
+ }
182
+ }
183
+
184
+ async function addTypedRecord(
185
+ auth: Auth,
186
+ data: TypedRecordData,
187
+ folderUid?: string
188
+ ): Promise<AddRecordResult> {
189
+ const recordUidBytes = generateUidBytes()
190
+ const recordKey = generateEncryptionKey()
191
+ const recordUid = webSafe64FromBytes(recordUidBytes)
192
+
193
+ const recordPayload = {
194
+ type: data.type,
195
+ title: data.title,
196
+ fields: data.fields || [],
197
+ custom: data.custom || [],
198
+ notes: data.notes || '',
199
+ }
200
+
201
+ const dataBytes = getPaddedJsonBytes(recordPayload)
202
+ const encryptedData = await platform.aesGcmEncrypt(dataBytes, recordKey)
203
+ const encryptedRecordKey = await platform.aesGcmEncrypt(recordKey, auth.dataKey!)
204
+
205
+ const recordAdd: Records.IRecordAdd = {
206
+ recordUid: recordUidBytes,
207
+ recordKey: encryptedRecordKey,
208
+ clientModifiedTime: Date.now(),
209
+ data: encryptedData,
210
+ folderType: Records.RecordFolderType.user_folder,
211
+ }
212
+
213
+ if (folderUid) {
214
+ recordAdd.folderUid = normal64Bytes(folderUid)
215
+ }
216
+
217
+ const request: Records.IRecordsAddRequest = {
218
+ records: [recordAdd],
219
+ clientTime: Date.now(),
220
+ }
221
+
222
+ const msg = recordsAddMessage(request)
223
+ const response = await auth.executeRest(msg)
224
+
225
+ const recordStatus = response.records?.[0]
226
+ const success =
227
+ recordStatus?.status === Records.RecordModifyResult.RS_SUCCESS ||
228
+ !recordStatus?.status
229
+
230
+ return {
231
+ recordUid,
232
+ success,
233
+ status: recordStatus?.status != null
234
+ ? Records.RecordModifyResult[recordStatus.status]
235
+ : ResultCode.OK,
236
+ }
237
+ }
238
+
239
+ export async function updateRecord(
240
+ auth: Auth,
241
+ recordUid: string,
242
+ data: TypedRecordData,
243
+ revision: number,
244
+ recordKey: Uint8Array
245
+ ): Promise<UpdateRecordResult> {
246
+ if (!data.title || !data.title.trim()) {
247
+ throw new KeeperSdkError('Record title is required.', 'missing_record_title')
248
+ }
249
+ if (!data.type?.trim()) {
250
+ throw new KeeperSdkError('Record type is required.', 'missing_record_type')
251
+ }
252
+ const recordUidBytes = normal64Bytes(recordUid)
253
+
254
+ const recordPayload = {
255
+ type: data.type,
256
+ title: data.title,
257
+ fields: data.fields || [],
258
+ custom: data.custom || [],
259
+ notes: data.notes || '',
260
+ }
261
+
262
+ const dataBytes = getPaddedJsonBytes(recordPayload)
263
+ const encryptedData = await platform.aesGcmEncrypt(dataBytes, recordKey)
264
+
265
+ const recordUpdate: Records.IRecordUpdate = {
266
+ recordUid: recordUidBytes,
267
+ clientModifiedTime: Date.now(),
268
+ revision,
269
+ data: encryptedData,
270
+ }
271
+
272
+ const request: Records.IRecordsUpdateRequest = {
273
+ records: [recordUpdate],
274
+ clientTime: Date.now(),
275
+ }
276
+
277
+ const msg = recordsUpdateMessage(request)
278
+ const response = await auth.executeRest(msg)
279
+
280
+ const recordStatus = response.records?.[0]
281
+ const success =
282
+ recordStatus?.status === Records.RecordModifyResult.RS_SUCCESS ||
283
+ !recordStatus?.status
284
+
285
+ return {
286
+ recordUid,
287
+ success,
288
+ status: recordStatus?.status != null
289
+ ? Records.RecordModifyResult[recordStatus.status]
290
+ : ResultCode.OK,
291
+ }
292
+ }
293
+
294
+ export async function deleteRecord(
295
+ auth: Auth,
296
+ recordUid: string
297
+ ): Promise<DeleteRecordResult> {
298
+ const preDeleteRequest = {
299
+ objects: [
300
+ {
301
+ object_uid: recordUid,
302
+ object_type: VaultObjectKind.Record,
303
+ from_uid: '',
304
+ from_type: FolderKind.UserFolder,
305
+ delete_resolution: DeleteResolution.Unlink,
306
+ } as RecordPreDeleteObject,
307
+ ],
308
+ }
309
+
310
+ let preDeleteResponse: KeeperPreDeleteResponse
311
+ try {
312
+ const preDeleteCmd = recordPreDeleteCommand(preDeleteRequest)
313
+ preDeleteResponse = await auth.executeRestCommand(preDeleteCmd)
314
+ } catch (err) {
315
+ return { recordUid, success: false, message: extractErrorMessage(err) }
316
+ }
317
+
318
+ const token = preDeleteResponse?.pre_delete_response?.pre_delete_token
319
+ if (!token) {
320
+ return {
321
+ recordUid,
322
+ success: false,
323
+ message: preDeleteResponse?.message || preDeleteResponse?.result_code || 'pre_delete failed: no token',
324
+ }
325
+ }
326
+
327
+ try {
328
+ const deleteCmd = recordDeleteCommand({ pre_delete_token: token })
329
+ await auth.executeRestCommand(deleteCmd)
330
+ } catch (err) {
331
+ return { recordUid, success: false, message: extractErrorMessage(err) }
332
+ }
333
+
334
+ return { recordUid, success: true, message: ResultCode.Success }
335
+ }
336
+
337
+ export type HistoryEntry = {
338
+ revision: number
339
+ version: number
340
+ userName: string
341
+ clientModifiedTime: number
342
+ data: Record<string, any> | null
343
+ }
344
+
345
+ export type RecordHistoryResult = {
346
+ recordUid: string
347
+ history: HistoryEntry[]
348
+ }
349
+
350
+ export async function getRecordHistory(
351
+ auth: Auth,
352
+ recordUid: string,
353
+ recordKey: Uint8Array
354
+ ): Promise<RecordHistoryResult> {
355
+ const cmd = getRecordHistoryCommand({
356
+ record_uid: recordUid,
357
+ client_time: Date.now(),
358
+ })
359
+
360
+ let response: GetRecordHistoryResponse
361
+ try {
362
+ response = await auth.executeRestCommand(cmd)
363
+ } catch (err) {
364
+ throw KeeperSdkError.from(err)
365
+ }
366
+
367
+ const rawHistory = response.history || []
368
+ const history: HistoryEntry[] = []
369
+
370
+ for (const entry of rawHistory) {
371
+ let decryptedData: Record<string, any> | null = null
372
+
373
+ if (entry.data) {
374
+ try {
375
+ const dataBytes = normal64Bytes(entry.data)
376
+ const version = entry.version || 0
377
+ let decrypted: Uint8Array
378
+
379
+ if (version <= RecordVersion.Legacy) {
380
+ decrypted = await platform.aesCbcDecrypt(dataBytes, recordKey, true)
381
+ } else {
382
+ decrypted = await platform.aesGcmDecrypt(dataBytes, recordKey)
383
+ }
384
+
385
+ decryptedData = JSON.parse(platform.bytesToString(decrypted))
386
+ } catch (err) {
387
+ logger.debug(`Failed to decrypt history revision ${entry.revision}:`, extractErrorMessage(err))
388
+ decryptedData = null
389
+ }
390
+ }
391
+
392
+ history.push({
393
+ revision: entry.revision,
394
+ version: entry.version,
395
+ userName: entry.user_name || '',
396
+ clientModifiedTime: entry.client_modified_time || 0,
397
+ data: decryptedData,
398
+ })
399
+ }
400
+
401
+ return { recordUid, history }
402
+ }
403
+
404
+ export type MoveRecordInput = {
405
+ recordUid: string
406
+ dstFolderUid: string
407
+ srcFolderUid?: string
408
+ link?: boolean
409
+ canEdit?: boolean
410
+ canShare?: boolean
411
+ }
412
+
413
+ export type MoveRecordResult = {
414
+ recordUid: string
415
+ success: boolean
416
+ message: string
417
+ }
418
+
419
+ type FolderInfo = {
420
+ uid: string
421
+ folderType: FolderKind
422
+ scopeUid: string
423
+ }
424
+
425
+ function resolveFolder(uid: string, storage: InMemoryStorage): FolderInfo {
426
+ if (!uid) {
427
+ return { uid: '', folderType: FolderKind.UserFolder, scopeUid: '' }
428
+ }
429
+
430
+ if (storage.getByUid<DUserFolder>(FolderKind.UserFolder, uid)) {
431
+ return { uid, folderType: FolderKind.UserFolder, scopeUid: '' }
432
+ }
433
+
434
+ if (storage.getByUid<DSharedFolder>(FolderKind.SharedFolder, uid)) {
435
+ return { uid, folderType: FolderKind.SharedFolder, scopeUid: uid }
436
+ }
437
+
438
+ const sfFolder = storage.getByUid<DSharedFolderFolder>(FolderKind.SharedFolderFolder, uid)
439
+ if (sfFolder) {
440
+ return { uid, folderType: FolderKind.SharedFolderFolder, scopeUid: sfFolder.sharedFolderUid }
441
+ }
442
+
443
+ return { uid, folderType: FolderKind.UserFolder, scopeUid: '' }
444
+ }
445
+
446
+ async function findRecordSourceFolder(recordUid: string, storage: InMemoryStorage): Promise<string> {
447
+ const folderKinds = [
448
+ FolderKind.UserFolder,
449
+ FolderKind.SharedFolder,
450
+ FolderKind.SharedFolderFolder,
451
+ ] as const
452
+
453
+ for (const kind of folderKinds) {
454
+ for (const folder of storage.getAll<DUserFolder | DSharedFolder | DSharedFolderFolder>(kind)) {
455
+ const dependencies = (await storage.getDependencies(folder.uid)) || []
456
+ if (
457
+ dependencies.some(
458
+ (dependency) => dependency.kind === VaultObjectKind.Record && dependency.uid === recordUid
459
+ )
460
+ ) {
461
+ return folder.uid
462
+ }
463
+ }
464
+ }
465
+
466
+ const sharedFolderRecord = storage
467
+ .getAll<DSharedFolderRecord>(VaultObjectKind.SharedFolderRecord)
468
+ .find((candidate) => candidate.recordUid === recordUid)
469
+ return sharedFolderRecord ? sharedFolderRecord.sharedFolderUid : ''
470
+ }
471
+
472
+ export async function moveRecord(
473
+ auth: Auth,
474
+ storage: InMemoryStorage,
475
+ input: MoveRecordInput
476
+ ): Promise<MoveRecordResult> {
477
+ const {
478
+ recordUid,
479
+ dstFolderUid,
480
+ link = false,
481
+ canEdit,
482
+ canShare,
483
+ } = input
484
+
485
+ const dst = resolveFolder(dstFolderUid, storage)
486
+
487
+ const srcUid =
488
+ input.srcFolderUid !== undefined ? input.srcFolderUid : await findRecordSourceFolder(recordUid, storage)
489
+ const src = resolveFolder(srcUid, storage)
490
+
491
+ const moveObj: MoveObject = {
492
+ uid: recordUid,
493
+ type: VaultObjectKind.Record,
494
+ cascade: false,
495
+ from_type: src.folderType,
496
+ from_uid: src.uid || undefined,
497
+ can_edit: canEdit,
498
+ can_reshare: canShare,
499
+ }
500
+
501
+ const transitionKeys: TransitionKeyObject[] = []
502
+
503
+ if (src.scopeUid !== dst.scopeUid) {
504
+ const recordKey = await storage.getKeyBytes(recordUid)
505
+ if (!recordKey) {
506
+ return { recordUid, success: false, message: 'Record key not found' }
507
+ }
508
+
509
+ let dstKey: Uint8Array | undefined
510
+ if (dst.scopeUid) {
511
+ dstKey = await storage.getKeyBytes(dst.scopeUid)
512
+ } else {
513
+ dstKey = auth.dataKey
514
+ }
515
+
516
+ if (!dstKey) {
517
+ return { recordUid, success: false, message: 'Destination folder key not found' }
518
+ }
519
+
520
+ const record = storage.getByUid<DRecord>(VaultObjectKind.Record, recordUid)
521
+ const version = record?.version || RecordVersion.Typed
522
+
523
+ let encryptedKey: Uint8Array
524
+ if (version >= RecordVersion.Typed) {
525
+ encryptedKey = await platform.aesGcmEncrypt(recordKey, dstKey)
526
+ } else {
527
+ encryptedKey = await platform.aesCbcEncrypt(recordKey, dstKey, true)
528
+ }
529
+
530
+ transitionKeys.push({ uid: recordUid, key: webSafe64FromBytes(encryptedKey) })
531
+ }
532
+
533
+ const request: MoveRequest = {
534
+ to_type: dst.folderType,
535
+ to_uid: dst.uid || undefined,
536
+ link,
537
+ move: [moveObj],
538
+ transition_keys: transitionKeys,
539
+ }
540
+
541
+ try {
542
+ const cmd = moveCommand(request)
543
+ await auth.executeRestCommand(cmd)
544
+ } catch (err) {
545
+ return { recordUid, success: false, message: extractErrorMessage(err) }
546
+ }
547
+
548
+ return { recordUid, success: true, message: 'Record moved successfully' }
549
+ }