@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,282 @@
|
|
|
1
|
+
import type { DRecord } from '@keeper-security/keeperapi'
|
|
2
|
+
import { getTotpCode } from './Totp'
|
|
3
|
+
|
|
4
|
+
enum FieldType {
|
|
5
|
+
Login = 'login',
|
|
6
|
+
Password = 'password',
|
|
7
|
+
Url = 'url',
|
|
8
|
+
Note = 'note',
|
|
9
|
+
Text = 'text',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum RecordVersion {
|
|
13
|
+
Legacy = 2,
|
|
14
|
+
Typed = 3,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TOTP_FIELD_TYPES = new Set<string>(['totp', 'oneTimeCode', 'otp'])
|
|
18
|
+
const MASKED_VALUE = '********'
|
|
19
|
+
const RECORD_SEPARATOR = '-'.repeat(50)
|
|
20
|
+
|
|
21
|
+
type RecordField = {
|
|
22
|
+
type: string
|
|
23
|
+
value: any[]
|
|
24
|
+
label?: string
|
|
25
|
+
required?: boolean
|
|
26
|
+
privacyScreen?: boolean
|
|
27
|
+
enforceGeneration?: boolean
|
|
28
|
+
complexity?: {
|
|
29
|
+
length?: number
|
|
30
|
+
caps?: number
|
|
31
|
+
lowercase?: number
|
|
32
|
+
digits?: number
|
|
33
|
+
special?: number
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type LegacyExtraField = {
|
|
38
|
+
type?: string
|
|
39
|
+
field_type?: string
|
|
40
|
+
value?: unknown
|
|
41
|
+
data?: unknown
|
|
42
|
+
label?: string
|
|
43
|
+
field_title?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toFieldValueArray(v: unknown): any[] {
|
|
47
|
+
if (v == null) return []
|
|
48
|
+
return Array.isArray(v) ? v : [v]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getLegacyExtraFields(record: DRecord): RecordField[] {
|
|
52
|
+
const raw = record.extra
|
|
53
|
+
if (raw == null) return []
|
|
54
|
+
let extra: { fields?: LegacyExtraField[] }
|
|
55
|
+
if (typeof raw === 'string') {
|
|
56
|
+
try {
|
|
57
|
+
extra = JSON.parse(raw)
|
|
58
|
+
} catch {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
} else if (typeof raw === 'object') {
|
|
62
|
+
extra = raw
|
|
63
|
+
} else {
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(extra.fields)) return []
|
|
67
|
+
const out: RecordField[] = []
|
|
68
|
+
for (const f of extra.fields) {
|
|
69
|
+
const typeName = f.type || f.field_type || FieldType.Text
|
|
70
|
+
const rawVal = f.value !== undefined && f.value !== null ? f.value : f.data
|
|
71
|
+
out.push({
|
|
72
|
+
type: typeName,
|
|
73
|
+
value: toFieldValueArray(rawVal),
|
|
74
|
+
label: f.label || f.field_title,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getRecordTitle(record: DRecord): string {
|
|
81
|
+
if (!record.data) return '(no data)'
|
|
82
|
+
if (typeof record.data === 'string') {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(record.data)
|
|
85
|
+
return parsed.title || '(untitled)'
|
|
86
|
+
} catch {
|
|
87
|
+
return '(parse error)'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return record.data.title || '(untitled)'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getRecordType(record: DRecord): string {
|
|
94
|
+
if (record.version <= RecordVersion.Legacy) return 'legacy'
|
|
95
|
+
if (!record.data) return 'unknown'
|
|
96
|
+
return record.data.type || 'unknown'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getRecordFields(record: DRecord): RecordField[] {
|
|
100
|
+
if (!record.data) return []
|
|
101
|
+
|
|
102
|
+
if (record.version <= RecordVersion.Legacy) {
|
|
103
|
+
const fields: RecordField[] = []
|
|
104
|
+
const d = record.data
|
|
105
|
+
if (d.secret1) fields.push({ type: FieldType.Login, value: [d.secret1] })
|
|
106
|
+
if (d.secret2) fields.push({ type: FieldType.Password, value: [d.secret2] })
|
|
107
|
+
if (d.link) fields.push({ type: FieldType.Url, value: [d.link] })
|
|
108
|
+
if (d.notes) fields.push({ type: FieldType.Note, value: [d.notes] })
|
|
109
|
+
if (Array.isArray(d.custom)) {
|
|
110
|
+
for (const c of d.custom) {
|
|
111
|
+
if (!c) continue
|
|
112
|
+
fields.push({
|
|
113
|
+
type: c.type || FieldType.Text,
|
|
114
|
+
value: c.value != null && c.value !== '' ? [c.value] : [],
|
|
115
|
+
label: c.name,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const extraFields = getLegacyExtraFields(record)
|
|
120
|
+
fields.push(...extraFields)
|
|
121
|
+
return fields
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const fields: RecordField[] = []
|
|
125
|
+
if (Array.isArray(record.data.fields)) {
|
|
126
|
+
for (const f of record.data.fields) {
|
|
127
|
+
fields.push({
|
|
128
|
+
type: f.type || FieldType.Text,
|
|
129
|
+
value: Array.isArray(f.value) ? f.value : [f.value],
|
|
130
|
+
label: f.label,
|
|
131
|
+
required: f.required,
|
|
132
|
+
privacyScreen: f.privacyScreen,
|
|
133
|
+
enforceGeneration: f.enforceGeneration,
|
|
134
|
+
complexity: f.complexity,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(record.data.custom)) {
|
|
139
|
+
for (const f of record.data.custom) {
|
|
140
|
+
fields.push({
|
|
141
|
+
type: f.type || FieldType.Text,
|
|
142
|
+
value: Array.isArray(f.value) ? f.value : [f.value],
|
|
143
|
+
label: f.label,
|
|
144
|
+
required: f.required,
|
|
145
|
+
privacyScreen: f.privacyScreen,
|
|
146
|
+
enforceGeneration: f.enforceGeneration,
|
|
147
|
+
complexity: f.complexity,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return fields
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type RecordSummary = {
|
|
155
|
+
login?: string
|
|
156
|
+
password?: string
|
|
157
|
+
url?: string
|
|
158
|
+
fields: RecordField[]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getRecordSummary(record: DRecord): RecordSummary {
|
|
162
|
+
const fields = getRecordFields(record)
|
|
163
|
+
if (record.version <= RecordVersion.Legacy) {
|
|
164
|
+
return {
|
|
165
|
+
login: record.data?.secret1,
|
|
166
|
+
password: record.data?.secret2,
|
|
167
|
+
url: record.data?.link,
|
|
168
|
+
fields,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let login: string | undefined
|
|
173
|
+
let password: string | undefined
|
|
174
|
+
let url: string | undefined
|
|
175
|
+
|
|
176
|
+
for (const f of fields) {
|
|
177
|
+
if (!login && f.type === FieldType.Login && f.value.length > 0) {
|
|
178
|
+
login = String(f.value[0])
|
|
179
|
+
} else if (!password && f.type === FieldType.Password && f.value.length > 0) {
|
|
180
|
+
password = String(f.value[0])
|
|
181
|
+
} else if (!url && f.type === FieldType.Url && f.value.length > 0) {
|
|
182
|
+
const val = f.value[0]
|
|
183
|
+
url = typeof val === 'string' ? val : val?.value || val?.url
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { login, password, url, fields }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getRecordTotpUrl(record: DRecord): string | undefined {
|
|
191
|
+
for (const field of getRecordFields(record)) {
|
|
192
|
+
if (!TOTP_FIELD_TYPES.has(field.type)) continue
|
|
193
|
+
for (const v of field.value) {
|
|
194
|
+
if (typeof v === 'string' && v.trim()) return v.trim()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getRecordPassword(record: DRecord): string | undefined {
|
|
201
|
+
return getRecordSummary(record).password
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getRecordLogin(record: DRecord): string | undefined {
|
|
205
|
+
return getRecordSummary(record).login
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function getRecordUrl(record: DRecord): string | undefined {
|
|
209
|
+
return getRecordSummary(record).url
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const wordCache = new WeakMap<DRecord, string[]>()
|
|
213
|
+
|
|
214
|
+
export function searchRecords(records: DRecord[], criteria: string): DRecord[] {
|
|
215
|
+
if (!criteria.trim()) return records
|
|
216
|
+
|
|
217
|
+
const searchWords = criteria.toLowerCase().split(/\s+/)
|
|
218
|
+
|
|
219
|
+
return records.filter((record) => {
|
|
220
|
+
let words = wordCache.get(record)
|
|
221
|
+
if (!words) {
|
|
222
|
+
words = collectRecordWords(record)
|
|
223
|
+
wordCache.set(record, words)
|
|
224
|
+
}
|
|
225
|
+
return searchWords.every((sw) => words!.some((w) => w.includes(sw)))
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function collectRecordWords(record: DRecord): string[] {
|
|
230
|
+
const words: string[] = []
|
|
231
|
+
const title = getRecordTitle(record)
|
|
232
|
+
if (title) words.push(...title.toLowerCase().split(/\s+/))
|
|
233
|
+
|
|
234
|
+
for (const field of getRecordFields(record)) {
|
|
235
|
+
if (field.label) words.push(field.label.toLowerCase())
|
|
236
|
+
for (const v of field.value) {
|
|
237
|
+
if (typeof v === 'string') {
|
|
238
|
+
words.push(...v.toLowerCase().split(/\s+/))
|
|
239
|
+
} else if (v && typeof v === 'object') {
|
|
240
|
+
for (const val of Object.values(v)) {
|
|
241
|
+
if (typeof val === 'string') {
|
|
242
|
+
words.push(...val.toLowerCase().split(/\s+/))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
words.push(record.uid)
|
|
250
|
+
return words
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function formatRecord(record: DRecord, showDetails = false): string {
|
|
254
|
+
const summary = getRecordSummary(record)
|
|
255
|
+
const lines: string[] = [
|
|
256
|
+
RECORD_SEPARATOR,
|
|
257
|
+
`Title: ${getRecordTitle(record)}`,
|
|
258
|
+
`Record UID: ${record.uid}`,
|
|
259
|
+
`Record Type: ${getRecordType(record)}`,
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
if (summary.login) lines.push(`Username: ${summary.login}`)
|
|
263
|
+
if (summary.url) lines.push(`URL: ${summary.url}`)
|
|
264
|
+
|
|
265
|
+
if (showDetails) {
|
|
266
|
+
for (const field of summary.fields) {
|
|
267
|
+
if (field.type === FieldType.Login || field.type === FieldType.Url) continue
|
|
268
|
+
const isTotp = TOTP_FIELD_TYPES.has(field.type)
|
|
269
|
+
const isSensitive = field.type === FieldType.Password || isTotp
|
|
270
|
+
const label = isTotp ? 'TOTP URL' : field.label || field.type
|
|
271
|
+
lines.push(`${label}: ${isSensitive ? MASKED_VALUE : field.value.join(', ')}`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const totpUrl = getRecordTotpUrl(record)
|
|
275
|
+
const code = totpUrl ? getTotpCode(totpUrl) : null
|
|
276
|
+
if (code) {
|
|
277
|
+
lines.push(`Two Factor Code: ${code.code} valid for ${code.secondsRemaining} sec`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return lines.join('\n')
|
|
282
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createHmac } from 'crypto'
|
|
2
|
+
|
|
3
|
+
export type TotpAlgorithm = 'SHA1' | 'SHA256' | 'SHA512'
|
|
4
|
+
|
|
5
|
+
export type TotpParams = {
|
|
6
|
+
secret: string
|
|
7
|
+
algorithm: TotpAlgorithm
|
|
8
|
+
digits: number
|
|
9
|
+
period: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TotpCode = {
|
|
13
|
+
code: string
|
|
14
|
+
secondsRemaining: number
|
|
15
|
+
period: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
19
|
+
const DEFAULT_DIGITS = 6
|
|
20
|
+
const DEFAULT_PERIOD = 30
|
|
21
|
+
const DEFAULT_ALGORITHM: TotpAlgorithm = 'SHA1'
|
|
22
|
+
const UINT32_MAX = 0x100000000
|
|
23
|
+
|
|
24
|
+
function decodeBase32(input: string): Uint8Array {
|
|
25
|
+
const noWhitespace = input.replace(/\s+/g, '')
|
|
26
|
+
let endIndex = noWhitespace.length
|
|
27
|
+
while (endIndex > 0 && noWhitespace.charCodeAt(endIndex - 1) === 0x3d) endIndex--
|
|
28
|
+
const cleaned = noWhitespace.slice(0, endIndex).toUpperCase()
|
|
29
|
+
const out: number[] = []
|
|
30
|
+
let buffer = 0
|
|
31
|
+
let bits = 0
|
|
32
|
+
for (const ch of cleaned) {
|
|
33
|
+
const idx = BASE32_ALPHABET.indexOf(ch)
|
|
34
|
+
if (idx < 0) throw new Error(`Invalid base32 character "${ch}" in TOTP secret`)
|
|
35
|
+
buffer = (buffer << 5) | idx
|
|
36
|
+
bits += 5
|
|
37
|
+
if (bits >= 8) {
|
|
38
|
+
bits -= 8
|
|
39
|
+
out.push((buffer >> bits) & 0xff)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Uint8Array.from(out)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeAlgorithm(value: string | null | undefined): TotpAlgorithm {
|
|
46
|
+
const upper = (value || DEFAULT_ALGORITHM).toUpperCase()
|
|
47
|
+
return upper === 'SHA256' || upper === 'SHA512' ? upper : DEFAULT_ALGORITHM
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parsePositiveInt(value: string | null, fallback: number): number {
|
|
51
|
+
const parsed = parseInt(value || '', 10)
|
|
52
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseTotpUrl(url: string): TotpParams | null {
|
|
56
|
+
if (!url?.trim()) return null
|
|
57
|
+
let params: URLSearchParams
|
|
58
|
+
try {
|
|
59
|
+
if (url.startsWith('otpauth://')) {
|
|
60
|
+
const u = new URL(url)
|
|
61
|
+
if (u.hostname.toLowerCase() !== 'totp') return null
|
|
62
|
+
params = u.searchParams
|
|
63
|
+
} else {
|
|
64
|
+
params = new URLSearchParams(url)
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
const secret = (params.get('secret') || '').trim()
|
|
70
|
+
if (!secret) return null
|
|
71
|
+
return {
|
|
72
|
+
secret,
|
|
73
|
+
algorithm: normalizeAlgorithm(params.get('algorithm')),
|
|
74
|
+
digits: parsePositiveInt(params.get('digits'), DEFAULT_DIGITS),
|
|
75
|
+
period: parsePositiveInt(params.get('period'), DEFAULT_PERIOD),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function counterToBuffer(counter: number): Buffer {
|
|
80
|
+
const buf = Buffer.alloc(8)
|
|
81
|
+
buf.writeUInt32BE(Math.floor(counter / UINT32_MAX), 0)
|
|
82
|
+
buf.writeUInt32BE(counter % UINT32_MAX, 4)
|
|
83
|
+
return buf
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getTotpCode(urlOrParams: string | TotpParams, now: number = Date.now()): TotpCode | null {
|
|
87
|
+
const params = typeof urlOrParams === 'string' ? parseTotpUrl(urlOrParams) : urlOrParams
|
|
88
|
+
if (!params) return null
|
|
89
|
+
if (!Number.isFinite(params.period) || params.period <= 0) return null
|
|
90
|
+
if (!Number.isFinite(params.digits) || params.digits <= 0) return null
|
|
91
|
+
|
|
92
|
+
let key: Uint8Array
|
|
93
|
+
try {
|
|
94
|
+
key = decodeBase32(params.secret)
|
|
95
|
+
} catch {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
if (key.length === 0) return null
|
|
99
|
+
|
|
100
|
+
const seconds = Math.floor(now / 1000)
|
|
101
|
+
const counter = Math.floor(seconds / params.period)
|
|
102
|
+
const secondsRemaining = params.period - (seconds % params.period)
|
|
103
|
+
|
|
104
|
+
const digest = createHmac(params.algorithm.toLowerCase(), Buffer.from(key))
|
|
105
|
+
.update(counterToBuffer(counter))
|
|
106
|
+
.digest()
|
|
107
|
+
|
|
108
|
+
if (digest.length === 0) return null
|
|
109
|
+
const offset = digest[digest.length - 1] & 0x0f
|
|
110
|
+
if (offset + 3 >= digest.length) return null
|
|
111
|
+
const binary =
|
|
112
|
+
((digest[offset] & 0x7f) << 24) |
|
|
113
|
+
((digest[offset + 1] & 0xff) << 16) |
|
|
114
|
+
((digest[offset + 2] & 0xff) << 8) |
|
|
115
|
+
(digest[offset + 3] & 0xff)
|
|
116
|
+
|
|
117
|
+
const code = (binary % 10 ** params.digits).toString().padStart(params.digits, '0')
|
|
118
|
+
return { code, secondsRemaining, period: params.period }
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Auth } from '@keeper-security/keeperapi'
|
|
2
|
+
import { InMemoryStorage } from '../storage/InMemoryStorage'
|
|
3
|
+
import { KeeperSdkError, ResultCodes } from '../utils'
|
|
4
|
+
import {
|
|
5
|
+
formatSharedFoldersTable,
|
|
6
|
+
listSharedFolders,
|
|
7
|
+
renderSharedFoldersAsciiTable,
|
|
8
|
+
} from './listSharedFolders'
|
|
9
|
+
import type {
|
|
10
|
+
FormattedSharedFoldersTable,
|
|
11
|
+
ListSharedFolderRow,
|
|
12
|
+
ListSharedFoldersOptions,
|
|
13
|
+
} from './listSharedFolders'
|
|
14
|
+
import { shareFolder } from './shareFolder'
|
|
15
|
+
import type { ShareFolderInput, ShareFolderResult } from './shareFolder'
|
|
16
|
+
|
|
17
|
+
export type AuthProvider = () => Auth
|
|
18
|
+
|
|
19
|
+
export class SharedFolderManager {
|
|
20
|
+
private readonly storage: InMemoryStorage
|
|
21
|
+
private readonly authProvider: AuthProvider
|
|
22
|
+
|
|
23
|
+
constructor(storage: InMemoryStorage, authProvider: AuthProvider) {
|
|
24
|
+
this.storage = storage
|
|
25
|
+
this.authProvider = authProvider
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private requireAuth(): Auth {
|
|
29
|
+
const auth = this.authProvider()
|
|
30
|
+
if (!auth) {
|
|
31
|
+
throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN)
|
|
32
|
+
}
|
|
33
|
+
return auth
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public listSharedFolders(options: ListSharedFoldersOptions = {}): ListSharedFolderRow[] {
|
|
37
|
+
return listSharedFolders(this.storage, options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public formatSharedFoldersTable(
|
|
41
|
+
rows: ListSharedFolderRow[],
|
|
42
|
+
options: { verbose?: boolean; columnWidth?: number } = {}
|
|
43
|
+
): FormattedSharedFoldersTable {
|
|
44
|
+
return formatSharedFoldersTable(rows, options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public renderSharedFoldersAsciiTable(
|
|
48
|
+
table: FormattedSharedFoldersTable,
|
|
49
|
+
options: { minColWidth?: number } = {}
|
|
50
|
+
): string {
|
|
51
|
+
return renderSharedFoldersAsciiTable(table, options)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async shareFolder(input: ShareFolderInput): Promise<ShareFolderResult> {
|
|
55
|
+
return shareFolder(this.requireAuth(), this.storage, input)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DSharedFolder,
|
|
3
|
+
DSharedFolderRecord,
|
|
4
|
+
DSharedFolderTeam,
|
|
5
|
+
DSharedFolderUser,
|
|
6
|
+
} from '@keeper-security/keeperapi'
|
|
7
|
+
import { InMemoryStorage } from '../storage/InMemoryStorage'
|
|
8
|
+
import { TOKEN_SEPARATOR_PATTERN } from '../utils'
|
|
9
|
+
import { FolderKind, VaultObjectKind } from '../folders/folderHelpers'
|
|
10
|
+
|
|
11
|
+
export type ListSharedFoldersOptions = {
|
|
12
|
+
pattern?: string | null
|
|
13
|
+
verbose?: boolean
|
|
14
|
+
includeDetails?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ListSharedFolderRow = {
|
|
18
|
+
shared_folder_uid: string
|
|
19
|
+
name: string
|
|
20
|
+
team_count?: number
|
|
21
|
+
user_count?: number
|
|
22
|
+
record_count?: number
|
|
23
|
+
default_manage_records?: boolean
|
|
24
|
+
default_manage_users?: boolean
|
|
25
|
+
default_can_edit?: boolean
|
|
26
|
+
default_can_share?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_COLUMN_WIDTH = 40
|
|
30
|
+
const MIN_TRUNCATE_PREFIX = 3
|
|
31
|
+
|
|
32
|
+
function sharedFolderDisplayName(folder: DSharedFolder): string {
|
|
33
|
+
return (folder.name || folder.uid).trim() || folder.uid
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findSharedFolders(storage: InMemoryStorage, pattern: string): DSharedFolder[] {
|
|
37
|
+
const searchWords = tokenize(pattern.toLowerCase())
|
|
38
|
+
const matches: DSharedFolder[] = []
|
|
39
|
+
for (const sharedFolder of storage.getAll<DSharedFolder>(FolderKind.SharedFolder)) {
|
|
40
|
+
const uid = sharedFolder.uid
|
|
41
|
+
const name = sharedFolderDisplayName(sharedFolder).toLowerCase()
|
|
42
|
+
const entityWords = tokenize(`${uid} ${name}`)
|
|
43
|
+
if (matchEntity(entityWords, searchWords)) {
|
|
44
|
+
matches.push(sharedFolder)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return matches
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matchEntity(entityWords: string[], searchWords: string[]): boolean {
|
|
51
|
+
if (!searchWords || searchWords.length === 0) return true
|
|
52
|
+
if (!entityWords || entityWords.length === 0) return false
|
|
53
|
+
for (const entityWord of entityWords) {
|
|
54
|
+
for (const searchWord of searchWords) {
|
|
55
|
+
if (searchWord.length <= entityWord.length && entityWord.includes(searchWord)) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function tokenize(text: string): string[] {
|
|
64
|
+
return text.split(TOKEN_SEPARATOR_PATTERN).filter((token) => token.length > 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function countBySharedFolderUid<T extends { sharedFolderUid: string }>(items: T[], sharedFolderUid: string): number {
|
|
68
|
+
let count = 0
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
if (item.sharedFolderUid === sharedFolderUid) count += 1
|
|
71
|
+
}
|
|
72
|
+
return count
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function countTeamsForFolder(storage: InMemoryStorage, sharedFolderUid: string): number {
|
|
76
|
+
return countBySharedFolderUid(storage.getAll<DSharedFolderTeam>(VaultObjectKind.SharedFolderTeam), sharedFolderUid)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function countUsersForFolder(storage: InMemoryStorage, sharedFolderUid: string): number {
|
|
80
|
+
return countBySharedFolderUid(storage.getAll<DSharedFolderUser>(VaultObjectKind.SharedFolderUser), sharedFolderUid)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function countRecordsForFolder(storage: InMemoryStorage, sharedFolderUid: string): number {
|
|
84
|
+
return countBySharedFolderUid(
|
|
85
|
+
storage.getAll<DSharedFolderRecord>(VaultObjectKind.SharedFolderRecord),
|
|
86
|
+
sharedFolderUid
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function listSharedFolders(
|
|
91
|
+
storage: InMemoryStorage,
|
|
92
|
+
options: ListSharedFoldersOptions = {}
|
|
93
|
+
): ListSharedFolderRow[] {
|
|
94
|
+
const { pattern, includeDetails = false } = options
|
|
95
|
+
const sharedFolders: DSharedFolder[] = pattern
|
|
96
|
+
? findSharedFolders(storage, pattern)
|
|
97
|
+
: storage.getAll<DSharedFolder>(FolderKind.SharedFolder)
|
|
98
|
+
|
|
99
|
+
return sharedFolders.map((sharedFolder) => {
|
|
100
|
+
const shared_folder_uid = sharedFolder.uid
|
|
101
|
+
const name = sharedFolderDisplayName(sharedFolder)
|
|
102
|
+
const row: ListSharedFolderRow = { shared_folder_uid, name }
|
|
103
|
+
if (includeDetails) {
|
|
104
|
+
row.record_count = countRecordsForFolder(storage, shared_folder_uid)
|
|
105
|
+
row.user_count = countUsersForFolder(storage, shared_folder_uid)
|
|
106
|
+
row.team_count = countTeamsForFolder(storage, shared_folder_uid)
|
|
107
|
+
row.default_manage_records = sharedFolder.defaultManageRecords
|
|
108
|
+
row.default_manage_users = sharedFolder.defaultManageUsers
|
|
109
|
+
row.default_can_edit = sharedFolder.defaultCanEdit
|
|
110
|
+
row.default_can_share = sharedFolder.defaultCanShare
|
|
111
|
+
}
|
|
112
|
+
return row
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type FormattedSharedFoldersTable = {
|
|
117
|
+
headers: string[]
|
|
118
|
+
rows: string[][]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function truncateText(text: string, maxLength: number): string {
|
|
122
|
+
if (!text) return ''
|
|
123
|
+
if (text.length <= maxLength) return text
|
|
124
|
+
if (maxLength <= MIN_TRUNCATE_PREFIX) return text.slice(0, maxLength)
|
|
125
|
+
return `${text.slice(0, maxLength - MIN_TRUNCATE_PREFIX)}...`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function formatSharedFoldersTable(
|
|
129
|
+
rows: ListSharedFolderRow[],
|
|
130
|
+
options: { verbose?: boolean; columnWidth?: number } = {}
|
|
131
|
+
): FormattedSharedFoldersTable {
|
|
132
|
+
const { verbose = false, columnWidth = DEFAULT_COLUMN_WIDTH } = options
|
|
133
|
+
const maxWidth = verbose ? null : columnWidth
|
|
134
|
+
const headers = ['#', 'Shared Folder UID', 'Name']
|
|
135
|
+
const outRows: string[][] = rows.map((row, rowIndex) => {
|
|
136
|
+
const uid = maxWidth == null ? row.shared_folder_uid : truncateText(row.shared_folder_uid, maxWidth)
|
|
137
|
+
const name = maxWidth == null ? row.name : truncateText(row.name, maxWidth)
|
|
138
|
+
return [String(rowIndex + 1), uid, name]
|
|
139
|
+
})
|
|
140
|
+
return { headers, rows: outRows }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function renderSharedFoldersAsciiTable(
|
|
144
|
+
table: FormattedSharedFoldersTable,
|
|
145
|
+
options: { minColWidth?: number } = {}
|
|
146
|
+
): string {
|
|
147
|
+
const { minColWidth = 2 } = options
|
|
148
|
+
const { headers, rows } = table
|
|
149
|
+
const columnCount = headers.length
|
|
150
|
+
const columnWidths: number[] = new Array(columnCount).fill(0)
|
|
151
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
152
|
+
columnWidths[columnIndex] = Math.max(headers[columnIndex].length, minColWidth)
|
|
153
|
+
}
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
156
|
+
const cell = row[columnIndex] || ''
|
|
157
|
+
columnWidths[columnIndex] = Math.max(columnWidths[columnIndex], cell.length, minColWidth)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const padCell = (cell: string, columnIndex: number) =>
|
|
161
|
+
cell + ' '.repeat(columnWidths[columnIndex] - cell.length)
|
|
162
|
+
const formatRow = (cells: string[]) => cells.map((cell, columnIndex) => padCell(cell, columnIndex)).join(' ')
|
|
163
|
+
const ruleRow = Array.from({ length: columnCount }, (_unused, columnIndex) =>
|
|
164
|
+
'-'.repeat(columnWidths[columnIndex])
|
|
165
|
+
)
|
|
166
|
+
.map((dashes, columnIndex) => padCell(dashes, columnIndex))
|
|
167
|
+
.join(' ')
|
|
168
|
+
const lines: string[] = [formatRow(headers), ruleRow]
|
|
169
|
+
for (const row of rows) {
|
|
170
|
+
lines.push(formatRow(row))
|
|
171
|
+
}
|
|
172
|
+
return lines.join('\n')
|
|
173
|
+
}
|