@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,351 @@
1
+ import readline from 'readline/promises'
2
+ import type { AuthUI3 } from '@keeper-security/keeperapi'
3
+ import { KeeperVault } from '../vault/KeeperVault'
4
+ import {
5
+ logger,
6
+ extractResultCode,
7
+ extractErrorMessage,
8
+ KeeperSdkError,
9
+ SdkDefaults,
10
+ AuthDefaults,
11
+ ResultCodes,
12
+ KEEPER_PUBLIC_HOSTS,
13
+ } from '../utils'
14
+ import { ConsoleAuthUI } from './ConsoleAuthUI'
15
+ import { FileConfigLoader } from './SessionManager'
16
+ import type { KeeperJsonConfig } from './SessionManager'
17
+
18
+ const DEFAULT_REGION = 'US'
19
+ const MASK_CHAR = '*'
20
+ const NOOP_WRITE = (() => true) as typeof process.stdout.write
21
+
22
+ enum CliCharAction {
23
+ Submit,
24
+ Cancel,
25
+ Backspace,
26
+ Append,
27
+ }
28
+
29
+ type ConsoleHandlers = {
30
+ log: typeof console.log
31
+ warn: typeof console.warn
32
+ debug: typeof console.debug
33
+ error: typeof console.error
34
+ stdoutWrite: typeof process.stdout.write
35
+ stderrWrite: typeof process.stderr.write
36
+ }
37
+
38
+ const defaultConfigLoader = new FileConfigLoader()
39
+
40
+ let rlManager: ReadlineManager | null = null
41
+ let suppressionDepth = 0
42
+ let originals: ConsoleHandlers | null = null
43
+
44
+ function captureConsoleHandlers(): ConsoleHandlers {
45
+ return {
46
+ log: console.log,
47
+ warn: console.warn,
48
+ debug: console.debug,
49
+ error: console.error,
50
+ stdoutWrite: process.stdout.write.bind(process.stdout),
51
+ stderrWrite: process.stderr.write.bind(process.stderr),
52
+ }
53
+ }
54
+
55
+ function applyConsoleHandlers(h: ConsoleHandlers): void {
56
+ console.log = h.log
57
+ console.warn = h.warn
58
+ console.debug = h.debug
59
+ console.error = h.error
60
+ process.stdout.write = h.stdoutWrite
61
+ process.stderr.write = h.stderrWrite
62
+ }
63
+
64
+ function classifyInputChar(ch: string): CliCharAction {
65
+ if (ch === '\n' || ch === '\r') return CliCharAction.Submit
66
+ if (ch === '\u0003') return CliCharAction.Cancel
67
+ if (ch === '\u007F' || ch === '\b') return CliCharAction.Backspace
68
+ return CliCharAction.Append
69
+ }
70
+
71
+ class ReadlineManager {
72
+ private rl: readline.Interface | null = null
73
+
74
+ private getOrCreate(): readline.Interface {
75
+ if (!this.rl) {
76
+ this.rl = readline.createInterface({
77
+ input: process.stdin,
78
+ output: process.stdout,
79
+ })
80
+ }
81
+ return this.rl
82
+ }
83
+
84
+ public async question(query: string): Promise<string> {
85
+ const rl = this.getOrCreate()
86
+ const answer = await rl.question(query)
87
+ return answer.trim()
88
+ }
89
+
90
+ public close(): void {
91
+ if (this.rl) {
92
+ this.rl.close()
93
+ this.rl = null
94
+ }
95
+ }
96
+ }
97
+
98
+ function getReadlineManager(): ReadlineManager {
99
+ if (!rlManager) {
100
+ rlManager = new ReadlineManager()
101
+ }
102
+ return rlManager
103
+ }
104
+
105
+ export function prompt(question: string, masked = false): Promise<string> {
106
+ const mgr = getReadlineManager()
107
+ if (!masked) {
108
+ return mgr.question(question)
109
+ }
110
+
111
+ return new Promise((resolve, reject) => {
112
+ mgr.close()
113
+ process.stdout.write(question)
114
+ let buf = ''
115
+ process.stdin.setRawMode(true)
116
+ process.stdin.resume()
117
+ process.stdin.setEncoding('utf8')
118
+
119
+ function exitRawMode() {
120
+ process.stdout.write('\n')
121
+ process.stdin.setRawMode(false)
122
+ process.stdin.pause()
123
+ process.stdin.removeListener('data', onData)
124
+ }
125
+
126
+ const onData = (str: string) => {
127
+ for (const ch of str) {
128
+ switch (classifyInputChar(ch)) {
129
+ case CliCharAction.Submit:
130
+ exitRawMode()
131
+ resolve(buf.trim())
132
+ return
133
+ case CliCharAction.Cancel:
134
+ exitRawMode()
135
+ reject(new KeeperSdkError('Operation cancelled by user.', ResultCodes.USER_CANCELLED))
136
+ return
137
+ case CliCharAction.Backspace:
138
+ if (buf.length > 0) {
139
+ buf = buf.slice(0, -1)
140
+ process.stdout.write('\b \b')
141
+ }
142
+ break
143
+ case CliCharAction.Append:
144
+ buf += ch
145
+ process.stdout.write(MASK_CHAR)
146
+ break
147
+ }
148
+ }
149
+ }
150
+
151
+ process.stdin.on('data', onData)
152
+ })
153
+ }
154
+
155
+ export async function loadKeeperConfig(preloaded?: KeeperJsonConfig): Promise<KeeperJsonConfig> {
156
+ if (preloaded) return preloaded
157
+ return defaultConfigLoader.load()
158
+ }
159
+
160
+ export async function resolveServer(username?: string, preloadedConfig?: KeeperJsonConfig): Promise<string> {
161
+ const config = await loadKeeperConfig(preloadedConfig)
162
+ const configServer = config.last_server || config.server
163
+
164
+ if (username) {
165
+ const users = config.users || []
166
+ const userEntry = users.find((u) => u.user?.toLowerCase() === username.toLowerCase())
167
+ if (userEntry?.server) return userEntry.server
168
+ }
169
+
170
+ if (configServer) return configServer
171
+
172
+ logger.info('Select server region:')
173
+ const entries = Object.entries(KEEPER_PUBLIC_HOSTS)
174
+ entries.forEach(([region, host], i) => {
175
+ logger.info(` ${i + 1}. ${region} (${host})`)
176
+ })
177
+ logger.info(` Or enter a hostname directly (e.g. dev.keepersecurity.com)`)
178
+
179
+ const choice = await prompt(`Region [1 = ${DEFAULT_REGION}]: `)
180
+ if (!choice) return KEEPER_PUBLIC_HOSTS[DEFAULT_REGION]
181
+
182
+ const idx = parseInt(choice, 10) - 1
183
+ if (idx >= 0 && idx < entries.length) return entries[idx][1]
184
+
185
+ return KEEPER_PUBLIC_HOSTS[choice.toUpperCase()] || choice
186
+ }
187
+
188
+ export function suppressLogs(): () => void {
189
+ if (suppressionDepth === 0) {
190
+ originals = captureConsoleHandlers()
191
+ applyConsoleHandlers({
192
+ log: () => {},
193
+ warn: () => {},
194
+ debug: () => {},
195
+ error: () => {},
196
+ stdoutWrite: NOOP_WRITE,
197
+ stderrWrite: NOOP_WRITE,
198
+ })
199
+ }
200
+ suppressionDepth++
201
+
202
+ let restored = false
203
+ return () => {
204
+ if (restored) return
205
+ restored = true
206
+ suppressionDepth--
207
+ if (suppressionDepth === 0 && originals) {
208
+ applyConsoleHandlers(originals)
209
+ originals = null
210
+ }
211
+ }
212
+ }
213
+
214
+ async function withSuppressedOutput<T>(fn: () => Promise<T>): Promise<T> {
215
+ const restore = suppressLogs()
216
+ try {
217
+ return await fn()
218
+ } finally {
219
+ restore()
220
+ }
221
+ }
222
+
223
+ function unsuppressLogs(): () => void {
224
+ if (suppressionDepth === 0 || !originals) return () => {}
225
+
226
+ const overrides = captureConsoleHandlers()
227
+ applyConsoleHandlers(originals)
228
+
229
+ let restored = false
230
+ return () => {
231
+ if (restored) return
232
+ restored = true
233
+ applyConsoleHandlers(overrides)
234
+ }
235
+ }
236
+
237
+ function unsuppressedAuthUI(): AuthUI3 {
238
+ const ui = new ConsoleAuthUI()
239
+ const wrap = <A extends unknown[], R>(fn: (...args: A) => Promise<R>) =>
240
+ async (...args: A): Promise<R> => {
241
+ const restore = unsuppressLogs()
242
+ try {
243
+ return await fn(...args)
244
+ } finally {
245
+ restore()
246
+ }
247
+ }
248
+ return {
249
+ waitForDeviceApproval: wrap(ui.waitForDeviceApproval.bind(ui)),
250
+ waitForTwoFactorCode: wrap(ui.waitForTwoFactorCode.bind(ui)),
251
+ getPassword: wrap(ui.getPassword.bind(ui)),
252
+ }
253
+ }
254
+
255
+ export async function login(): Promise<KeeperVault> {
256
+ const config = await loadKeeperConfig()
257
+ const defaultUsername = config.last_login || config.user || ''
258
+
259
+ const host = defaultUsername ? await resolveServer(defaultUsername, config) : undefined
260
+
261
+ if (defaultUsername && host) {
262
+ const vault = await tryPersistentLogin(host, defaultUsername)
263
+ if (vault) return vault
264
+ }
265
+
266
+ let username: string
267
+ if (defaultUsername) {
268
+ logger.info(`Enter master password for ${defaultUsername}`)
269
+ username = defaultUsername
270
+ } else {
271
+ username = await prompt('Username (email): ')
272
+ }
273
+
274
+ if (!username) {
275
+ throw new KeeperSdkError('Username is required.', ResultCodes.MISSING_USERNAME)
276
+ }
277
+
278
+ const resolvedHost = host || (await resolveServer(username, config))
279
+ return await interactiveLogin(resolvedHost, username)
280
+ }
281
+
282
+ async function tryPersistentLogin(host: string, username: string): Promise<KeeperVault | null> {
283
+ const vault = new KeeperVault({
284
+ host,
285
+ clientVersion: SdkDefaults.CLIENT_VERSION,
286
+ authUI: unsuppressedAuthUI(),
287
+ })
288
+ try {
289
+ await withSuppressedOutput(() => vault.resumeSession())
290
+ logger.info(`Logging in to Keeper as ${username}`)
291
+ logger.info('Successfully authenticated with Persistent Login')
292
+ return await syncVault(vault)
293
+ } catch (err) {
294
+ logger.debug('Persistent login failed:', extractErrorMessage(err))
295
+ vault.disconnect()
296
+ return null
297
+ }
298
+ }
299
+
300
+ async function interactiveLogin(host: string, username: string): Promise<KeeperVault> {
301
+ const vault = new KeeperVault({
302
+ host,
303
+ clientVersion: SdkDefaults.CLIENT_VERSION,
304
+ authUI: unsuppressedAuthUI(),
305
+ })
306
+
307
+ for (let attempt = 1; attempt <= AuthDefaults.MAX_LOGIN_ATTEMPTS; attempt++) {
308
+ const password = await prompt('Password: ', true)
309
+
310
+ if (!password) {
311
+ throw new KeeperSdkError('Password is required.', ResultCodes.MISSING_PASSWORD)
312
+ }
313
+
314
+ try {
315
+ await withSuppressedOutput(() => vault.login(username, password))
316
+ logger.info('Successfully authenticated with Master Password\n')
317
+ return await syncVault(vault)
318
+ } catch (err) {
319
+ const resultCode = extractResultCode(err)
320
+ if (resultCode === ResultCodes.INVALID_CREDENTIALS) {
321
+ const remaining = AuthDefaults.MAX_LOGIN_ATTEMPTS - attempt
322
+ if (remaining > 0) {
323
+ logger.warn(`Invalid credentials (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`)
324
+ continue
325
+ }
326
+ throw new KeeperSdkError(
327
+ `Maximum login attempts (${AuthDefaults.MAX_LOGIN_ATTEMPTS}) exceeded.`,
328
+ ResultCodes.MAX_ATTEMPTS_EXCEEDED
329
+ )
330
+ }
331
+ throw KeeperSdkError.from(err)
332
+ }
333
+ }
334
+
335
+ throw new KeeperSdkError(
336
+ `Maximum login attempts (${AuthDefaults.MAX_LOGIN_ATTEMPTS}) exceeded.`,
337
+ ResultCodes.MAX_ATTEMPTS_EXCEEDED
338
+ )
339
+ }
340
+
341
+ async function syncVault(vault: KeeperVault): Promise<KeeperVault> {
342
+ logger.info('Syncing vault...')
343
+ await withSuppressedOutput(() => vault.sync())
344
+ logger.info(`Vault synced. ${vault.getSummary().recordCount} records loaded.\n`)
345
+ return vault
346
+ }
347
+
348
+ export function cleanup(vault: KeeperVault): void {
349
+ vault.disconnect()
350
+ getReadlineManager().close()
351
+ }
@@ -0,0 +1,293 @@
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import {
5
+ normal64Bytes,
6
+ type DeviceConfig,
7
+ type SessionStorage,
8
+ type KeeperHost,
9
+ type SessionParams,
10
+ } from '@keeper-security/keeperapi'
11
+ import { logger, extractErrorMessage, SdkDefaults } from '../utils'
12
+ import type { Nullable } from '../utils'
13
+
14
+ export type ConfigurationUser = {
15
+ user?: string
16
+ server?: string
17
+ last_device?: { device_token?: string }
18
+ }
19
+
20
+ export type ConfigurationServerConfig = {
21
+ server?: string
22
+ clone_code?: string
23
+ }
24
+
25
+ export type ConfigurationDeviceConfig = {
26
+ device_token?: string
27
+ private_key?: string
28
+ server_info?: Array<ConfigurationServerConfig>
29
+ }
30
+
31
+ export type KeeperJsonConfig = {
32
+ last_login?: string
33
+ last_server?: string
34
+ user?: string
35
+ server?: string
36
+ device_token?: string
37
+ private_key?: string
38
+ clone_code?: string
39
+ users?: Array<ConfigurationUser>
40
+ devices?: Array<ConfigurationDeviceConfig>
41
+ }
42
+
43
+ type ResolvedDevice = {
44
+ deviceToken: Uint8Array
45
+ privateKey: Uint8Array
46
+ serverInfo: Array<Required<ConfigurationServerConfig>>
47
+ }
48
+
49
+ type DeviceCacheEntry = {
50
+ username: string
51
+ device: Nullable<ResolvedDevice>
52
+ }
53
+
54
+ export interface ConfigLoader {
55
+ load(): Promise<KeeperJsonConfig>
56
+ save(config: KeeperJsonConfig): Promise<void>
57
+ readonly configDir: string
58
+ }
59
+
60
+ export class FileConfigLoader implements ConfigLoader {
61
+ public readonly configDir: string
62
+
63
+ constructor(configDir?: string) {
64
+ this.configDir = configDir || path.join(os.homedir(), SdkDefaults.CONFIG_DIR)
65
+ }
66
+
67
+ async load(): Promise<KeeperJsonConfig> {
68
+ const configPath = path.join(this.configDir, 'config.json')
69
+ try {
70
+ const content = await fs.readFile(configPath, 'utf-8')
71
+ const parsed: unknown = JSON.parse(content)
72
+ if (SessionManager.isValidKeeperConfig(parsed)) {
73
+ return parsed
74
+ }
75
+ } catch (err) {
76
+ logger.debug('Failed to load keeper config:', extractErrorMessage(err))
77
+ }
78
+ return {}
79
+ }
80
+
81
+ async save(config: KeeperJsonConfig): Promise<void> {
82
+ const configPath = path.join(this.configDir, 'config.json')
83
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), {
84
+ mode: 0o600,
85
+ })
86
+ }
87
+ }
88
+
89
+ export class SessionManager implements SessionStorage {
90
+ private readonly configLoader: ConfigLoader
91
+ private sessionParams: Nullable<SessionParams> = null
92
+ private _lastUsername?: string
93
+ private _keeperConfig: Nullable<KeeperJsonConfig> = null
94
+ private _deviceCache: Nullable<DeviceCacheEntry> = null
95
+ private sessionDevices = new Map<string, DeviceConfig>()
96
+ private sessionCloneCodes = new Map<string, Uint8Array>()
97
+
98
+ constructor(configDir?: string)
99
+ constructor(loader: ConfigLoader)
100
+ constructor(configDirOrLoader?: string | ConfigLoader) {
101
+ if (typeof configDirOrLoader === 'string' || configDirOrLoader === undefined) {
102
+ this.configLoader = new FileConfigLoader(configDirOrLoader as string | undefined)
103
+ } else {
104
+ this.configLoader = configDirOrLoader
105
+ }
106
+ }
107
+
108
+ public get configDir(): string {
109
+ return this.configLoader.configDir
110
+ }
111
+
112
+ public get lastUsername(): string | undefined {
113
+ return this._lastUsername
114
+ }
115
+
116
+ public async getLastUsername(): Promise<string | undefined> {
117
+ if (this._lastUsername) return this._lastUsername
118
+ const keeperConfig = await this.loadKeeperConfig()
119
+ return keeperConfig.last_login || keeperConfig.user || undefined
120
+ }
121
+
122
+ public async getDeviceConfig(host: string): Promise<DeviceConfig> {
123
+ const username = await this.getLastUsername()
124
+ if (username) {
125
+ const device = await this.findDeviceInKeeperConfig(username)
126
+ if (device) {
127
+ return {
128
+ deviceToken: device.deviceToken,
129
+ privateKey: device.privateKey,
130
+ }
131
+ }
132
+ }
133
+
134
+ return this.sessionDevices.get(host) || {}
135
+ }
136
+
137
+ public createOnDeviceConfig(host: string): (deviceConfig: DeviceConfig) => Promise<void> {
138
+ return async (deviceConfig: DeviceConfig) => {
139
+ this.sessionDevices.set(host, { ...deviceConfig })
140
+ }
141
+ }
142
+
143
+ private cloneCodeKey(host: KeeperHost, username: string): string {
144
+ return `${host}::${username}`
145
+ }
146
+
147
+ public async getCloneCode(host: KeeperHost, username: string): Promise<Nullable<Uint8Array>> {
148
+ const hostStr = String(host)
149
+
150
+ const key = this.cloneCodeKey(host, username)
151
+ const sessionCode = this.sessionCloneCodes.get(key)
152
+ if (sessionCode) return sessionCode
153
+
154
+ const device = await this.findDeviceInKeeperConfig(username)
155
+ if (device) {
156
+ const serverInfo = device.serverInfo.find((entry) => entry.server === hostStr)
157
+ if (serverInfo) {
158
+ return normal64Bytes(serverInfo.clone_code)
159
+ }
160
+ }
161
+
162
+ return null
163
+ }
164
+
165
+ public async saveCloneCode(host: KeeperHost, username: string, cloneCode: Uint8Array): Promise<void> {
166
+ const key = this.cloneCodeKey(host, username)
167
+ this.sessionCloneCodes.set(key, cloneCode)
168
+ await this.updateKeeperConfigCloneCode(String(host), username, cloneCode)
169
+ }
170
+
171
+ private async updateKeeperConfigCloneCode(host: string, username: string, cloneCode: Uint8Array): Promise<void> {
172
+ try {
173
+ const parsed = await this.configLoader.load()
174
+ if (!parsed || Object.keys(parsed).length === 0) return
175
+
176
+ let updated = false
177
+ const encodedCloneCode = Buffer.from(cloneCode).toString('base64url')
178
+
179
+ const server = parsed.last_server || parsed.server
180
+ if (parsed.user?.toLowerCase() === username.toLowerCase() && server === host) {
181
+ parsed.clone_code = encodedCloneCode
182
+ updated = true
183
+ }
184
+
185
+ const user = (parsed.users || []).find(
186
+ (configUser) => configUser.user?.toLowerCase() === username.toLowerCase()
187
+ )
188
+ if (user?.last_device?.device_token) {
189
+ const device = (parsed.devices || []).find(
190
+ (configDevice) => configDevice.device_token === user.last_device.device_token
191
+ )
192
+ if (device?.server_info) {
193
+ const serverInfo = device.server_info.find((entry) => entry.server === host)
194
+ if (serverInfo) {
195
+ serverInfo.clone_code = encodedCloneCode
196
+ updated = true
197
+ }
198
+ }
199
+ }
200
+
201
+ if (updated) {
202
+ await this.configLoader.save(parsed)
203
+ this._keeperConfig = null
204
+ this._deviceCache = null
205
+ }
206
+ } catch (err) {
207
+ logger.warn('Failed to update keeper config clone code:', extractErrorMessage(err))
208
+ }
209
+ }
210
+
211
+ public async getSessionParameters(): Promise<Nullable<SessionParams>> {
212
+ return this.sessionParams
213
+ }
214
+
215
+ public async saveSessionParameters(params: Partial<SessionParams>): Promise<void> {
216
+ this.sessionParams = { ...this.sessionParams, ...params } as SessionParams
217
+ if (params.username) {
218
+ this._lastUsername = params.username
219
+ }
220
+ }
221
+
222
+ public setLastUsername(username: string): void {
223
+ this._lastUsername = username
224
+ }
225
+
226
+ private async loadKeeperConfig(): Promise<KeeperJsonConfig> {
227
+ if (this._keeperConfig) return this._keeperConfig
228
+ this._keeperConfig = await this.configLoader.load()
229
+ return this._keeperConfig
230
+ }
231
+
232
+ private async findDeviceInKeeperConfig(username: string): Promise<Nullable<ResolvedDevice>> {
233
+ const normalizedUsername = username.toLowerCase()
234
+ if (this._deviceCache?.username === normalizedUsername) {
235
+ return this._deviceCache.device
236
+ }
237
+
238
+ const device = await this.lookupDeviceInKeeperConfig(normalizedUsername)
239
+ this._deviceCache = { username: normalizedUsername, device }
240
+ return device
241
+ }
242
+
243
+ private async lookupDeviceInKeeperConfig(normalizedUsername: string): Promise<Nullable<ResolvedDevice>> {
244
+ const keeperConfig = await this.loadKeeperConfig()
245
+
246
+ if (keeperConfig.users && keeperConfig.devices) {
247
+ const user = keeperConfig.users.find(
248
+ (configUser) => configUser.user?.toLowerCase() === normalizedUsername
249
+ )
250
+ if (user?.last_device?.device_token) {
251
+ const deviceTokenStr = user.last_device.device_token
252
+ const device = keeperConfig.devices.find((configDevice) => configDevice.device_token === deviceTokenStr)
253
+ if (device?.private_key) {
254
+ return {
255
+ deviceToken: normal64Bytes(deviceTokenStr),
256
+ privateKey: normal64Bytes(device.private_key),
257
+ serverInfo: (device.server_info || []).filter(
258
+ (entry): entry is Required<ConfigurationServerConfig> =>
259
+ !!entry.server && !!entry.clone_code
260
+ ),
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ if (
267
+ keeperConfig.device_token &&
268
+ keeperConfig.private_key &&
269
+ keeperConfig.user?.toLowerCase() === normalizedUsername
270
+ ) {
271
+ const serverInfo: Array<Required<ConfigurationServerConfig>> = []
272
+ const server = keeperConfig.last_server || keeperConfig.server
273
+ if (server && keeperConfig.clone_code) {
274
+ serverInfo.push({ server, clone_code: keeperConfig.clone_code })
275
+ }
276
+ return {
277
+ deviceToken: normal64Bytes(keeperConfig.device_token),
278
+ privateKey: normal64Bytes(keeperConfig.private_key),
279
+ serverInfo,
280
+ }
281
+ }
282
+
283
+ return null
284
+ }
285
+
286
+ public static isValidKeeperConfig(value: unknown): value is KeeperJsonConfig {
287
+ if (typeof value !== 'object' || value === null) return false
288
+ const obj = value as Record<string, unknown>
289
+ if (obj.users !== undefined && !Array.isArray(obj.users)) return false
290
+ if (obj.devices !== undefined && !Array.isArray(obj.devices)) return false
291
+ return true
292
+ }
293
+ }