@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,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
+ }