@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.
- package/dist/auth/ConsoleAuthUI.d.ts +10 -0
- package/dist/auth/ConsoleAuthUI.js +152 -0
- package/dist/auth/ConsoleAuthUI.js.map +1 -0
- package/dist/auth/ConsoleLogin.d.ts +8 -0
- package/dist/auth/ConsoleLogin.js +266 -0
- package/dist/auth/ConsoleLogin.js.map +1 -0
- package/dist/auth/SessionManager.d.ts +66 -0
- package/dist/auth/SessionManager.js +211 -0
- package/dist/auth/SessionManager.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/records/RecordOperations.d.ts +79 -0
- package/dist/records/RecordOperations.js +346 -0
- package/dist/records/RecordOperations.js.map +1 -0
- package/dist/records/RecordUtils.d.ts +36 -0
- package/dist/records/RecordUtils.js +224 -0
- package/dist/records/RecordUtils.js.map +1 -0
- package/dist/sharing/Sharing.d.ts +27 -0
- package/dist/sharing/Sharing.js +125 -0
- package/dist/sharing/Sharing.js.map +1 -0
- package/dist/src/auth/ConsoleAuthUI.d.ts +10 -0
- package/dist/src/auth/ConsoleAuthUI.js +161 -0
- package/dist/src/auth/ConsoleAuthUI.js.map +1 -0
- package/dist/src/auth/ConsoleLogin.d.ts +8 -0
- package/dist/src/auth/ConsoleLogin.js +311 -0
- package/dist/src/auth/ConsoleLogin.js.map +1 -0
- package/dist/src/auth/SessionManager.d.ts +67 -0
- package/dist/src/auth/SessionManager.js +212 -0
- package/dist/src/auth/SessionManager.js.map +1 -0
- package/dist/src/folders/FolderManager.d.ts +57 -0
- package/dist/src/folders/FolderManager.js +108 -0
- package/dist/src/folders/FolderManager.js.map +1 -0
- package/dist/src/folders/addFolder.d.ts +32 -0
- package/dist/src/folders/addFolder.js +207 -0
- package/dist/src/folders/addFolder.js.map +1 -0
- package/dist/src/folders/changeDirectory.d.ts +19 -0
- package/dist/src/folders/changeDirectory.js +171 -0
- package/dist/src/folders/changeDirectory.js.map +1 -0
- package/dist/src/folders/deleteFolder.d.ts +17 -0
- package/dist/src/folders/deleteFolder.js +237 -0
- package/dist/src/folders/deleteFolder.js.map +1 -0
- package/dist/src/folders/folderHelpers.d.ts +48 -0
- package/dist/src/folders/folderHelpers.js +100 -0
- package/dist/src/folders/folderHelpers.js.map +1 -0
- package/dist/src/folders/folderTree.d.ts +29 -0
- package/dist/src/folders/folderTree.js +250 -0
- package/dist/src/folders/folderTree.js.map +1 -0
- package/dist/src/folders/getFolder.d.ts +56 -0
- package/dist/src/folders/getFolder.js +143 -0
- package/dist/src/folders/getFolder.js.map +1 -0
- package/dist/src/folders/listFolder.d.ts +48 -0
- package/dist/src/folders/listFolder.js +276 -0
- package/dist/src/folders/listFolder.js.map +1 -0
- package/dist/src/folders/updateFolder.d.ts +31 -0
- package/dist/src/folders/updateFolder.js +137 -0
- package/dist/src/folders/updateFolder.js.map +1 -0
- package/dist/src/index.d.ts +49 -0
- package/dist/src/index.js +151 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/records/RecordOperations.d.ts +80 -0
- package/dist/src/records/RecordOperations.js +356 -0
- package/dist/src/records/RecordOperations.js.map +1 -0
- package/dist/src/records/RecordUtils.d.ts +37 -0
- package/dist/src/records/RecordUtils.js +263 -0
- package/dist/src/records/RecordUtils.js.map +1 -0
- package/dist/src/records/Totp.d.ts +14 -0
- package/dist/src/records/Totp.js +111 -0
- package/dist/src/records/Totp.js.map +1 -0
- package/dist/src/sharedFolders/SharedFolderManager.d.ts +20 -0
- package/dist/src/sharedFolders/SharedFolderManager.js +33 -0
- package/dist/src/sharedFolders/SharedFolderManager.js.map +1 -0
- package/dist/src/sharedFolders/listSharedFolders.d.ts +29 -0
- package/dist/src/sharedFolders/listSharedFolders.js +127 -0
- package/dist/src/sharedFolders/listSharedFolders.js.map +1 -0
- package/dist/src/sharedFolders/shareFolder.d.ts +36 -0
- package/dist/src/sharedFolders/shareFolder.js +352 -0
- package/dist/src/sharedFolders/shareFolder.js.map +1 -0
- package/dist/src/sharing/Sharing.d.ts +50 -0
- package/dist/src/sharing/Sharing.js +195 -0
- package/dist/src/sharing/Sharing.js.map +1 -0
- package/dist/src/storage/InMemoryStorage.d.ts +24 -0
- package/dist/src/storage/InMemoryStorage.js +139 -0
- package/dist/src/storage/InMemoryStorage.js.map +1 -0
- package/dist/src/teams/TeamManager.d.ts +17 -0
- package/dist/src/teams/TeamManager.js +38 -0
- package/dist/src/teams/TeamManager.js.map +1 -0
- package/dist/src/teams/enterpriseData.d.ts +106 -0
- package/dist/src/teams/enterpriseData.js +319 -0
- package/dist/src/teams/enterpriseData.js.map +1 -0
- package/dist/src/teams/listTeams.d.ts +42 -0
- package/dist/src/teams/listTeams.js +308 -0
- package/dist/src/teams/listTeams.js.map +1 -0
- package/dist/src/teams/viewTeam.d.ts +35 -0
- package/dist/src/teams/viewTeam.js +177 -0
- package/dist/src/teams/viewTeam.js.map +1 -0
- package/dist/src/utils/Logger.d.ts +28 -0
- package/dist/src/utils/Logger.js +62 -0
- package/dist/src/utils/Logger.js.map +1 -0
- package/dist/src/utils/constants.d.ts +50 -0
- package/dist/src/utils/constants.js +64 -0
- package/dist/src/utils/constants.js.map +1 -0
- package/dist/src/utils/errors.d.ts +10 -0
- package/dist/src/utils/errors.js +117 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/guards.d.ts +7 -0
- package/dist/src/utils/guards.js +29 -0
- package/dist/src/utils/guards.js.map +1 -0
- package/dist/src/utils/index.d.ts +7 -0
- package/dist/src/utils/index.js +39 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/patterns.d.ts +9 -0
- package/dist/src/utils/patterns.js +20 -0
- package/dist/src/utils/patterns.js.map +1 -0
- package/dist/src/utils/types.d.ts +12 -0
- package/dist/src/utils/types.js +3 -0
- package/dist/src/utils/types.js.map +1 -0
- package/dist/src/vault/KeeperVault.d.ts +116 -0
- package/dist/src/vault/KeeperVault.js +443 -0
- package/dist/src/vault/KeeperVault.js.map +1 -0
- package/dist/storage/InMemoryStorage.d.ts +24 -0
- package/dist/storage/InMemoryStorage.js +132 -0
- package/dist/storage/InMemoryStorage.js.map +1 -0
- package/dist/utils/Logger.d.ts +28 -0
- package/dist/utils/Logger.js +62 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/constants.d.ts +26 -0
- package/dist/utils/constants.js +37 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/errors.d.ts +10 -0
- package/dist/utils/errors.js +117 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +22 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/vault/KeeperVault.d.ts +72 -0
- package/dist/vault/KeeperVault.js +338 -0
- package/dist/vault/KeeperVault.js.map +1 -0
- package/package.json +32 -0
- package/src/auth/ConsoleAuthUI.ts +169 -0
- package/src/auth/ConsoleLogin.ts +351 -0
- package/src/auth/SessionManager.ts +293 -0
- package/src/folders/FolderManager.ts +174 -0
- package/src/folders/addFolder.ts +294 -0
- package/src/folders/changeDirectory.ts +217 -0
- package/src/folders/deleteFolder.ts +293 -0
- package/src/folders/folderHelpers.ts +99 -0
- package/src/folders/folderTree.ts +321 -0
- package/src/folders/getFolder.ts +234 -0
- package/src/folders/listFolder.ts +358 -0
- package/src/folders/updateFolder.ts +210 -0
- package/src/index.ts +242 -0
- package/src/records/RecordOperations.ts +549 -0
- package/src/records/RecordUtils.ts +282 -0
- package/src/records/Totp.ts +119 -0
- package/src/sharedFolders/SharedFolderManager.ts +57 -0
- package/src/sharedFolders/listSharedFolders.ts +173 -0
- package/src/sharedFolders/shareFolder.ts +457 -0
- package/src/sharing/Sharing.ts +282 -0
- package/src/storage/InMemoryStorage.ts +163 -0
- package/src/teams/TeamManager.ts +61 -0
- package/src/teams/enterpriseData.ts +453 -0
- package/src/teams/listTeams.ts +373 -0
- package/src/teams/viewTeam.ts +248 -0
- package/src/utils/Logger.ts +71 -0
- package/src/utils/constants.ts +63 -0
- package/src/utils/errors.ts +108 -0
- package/src/utils/guards.ts +24 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/patterns.ts +20 -0
- package/src/utils/types.ts +11 -0
- package/src/vault/KeeperVault.ts +612 -0
- 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
|
+
}
|