oro-sdk 2.1.4-dev1.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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/dist/client.d.ts +464 -0
  4. package/dist/helpers/client.d.ts +23 -0
  5. package/dist/helpers/index.d.ts +4 -0
  6. package/dist/helpers/patient-registration.d.ts +16 -0
  7. package/dist/helpers/vault-grants.d.ts +20 -0
  8. package/dist/helpers/workflow.d.ts +23 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.js +8 -0
  11. package/dist/models/client.d.ts +28 -0
  12. package/dist/models/consult.d.ts +102 -0
  13. package/dist/models/diagnosis.d.ts +122 -0
  14. package/dist/models/error.d.ts +26 -0
  15. package/dist/models/guard.d.ts +119 -0
  16. package/dist/models/index.d.ts +9 -0
  17. package/dist/models/practice.d.ts +353 -0
  18. package/dist/models/shared.d.ts +8 -0
  19. package/dist/models/vault.d.ts +124 -0
  20. package/dist/models/workflow.d.ts +106 -0
  21. package/dist/oro-sdk.cjs.development.js +7685 -0
  22. package/dist/oro-sdk.cjs.development.js.map +1 -0
  23. package/dist/oro-sdk.cjs.production.min.js +2 -0
  24. package/dist/oro-sdk.cjs.production.min.js.map +1 -0
  25. package/dist/oro-sdk.esm.js +7692 -0
  26. package/dist/oro-sdk.esm.js.map +1 -0
  27. package/dist/sdk-revision/client.d.ts +21 -0
  28. package/dist/sdk-revision/index.d.ts +1 -0
  29. package/dist/services/api.d.ts +11 -0
  30. package/dist/services/axios.d.ts +14 -0
  31. package/dist/services/consult.d.ts +54 -0
  32. package/dist/services/diagnosis.d.ts +44 -0
  33. package/dist/services/external/clinia.d.ts +82 -0
  34. package/dist/services/external/index.d.ts +1 -0
  35. package/dist/services/guard.d.ts +92 -0
  36. package/dist/services/index.d.ts +10 -0
  37. package/dist/services/practice.d.ts +100 -0
  38. package/dist/services/teller.d.ts +9 -0
  39. package/dist/services/vault.d.ts +54 -0
  40. package/dist/services/workflow.d.ts +21 -0
  41. package/package.json +63 -0
  42. package/src/client.ts +1843 -0
  43. package/src/helpers/client.ts +199 -0
  44. package/src/helpers/index.ts +4 -0
  45. package/src/helpers/patient-registration.ts +490 -0
  46. package/src/helpers/vault-grants.ts +51 -0
  47. package/src/helpers/workflow.ts +261 -0
  48. package/src/index.ts +61 -0
  49. package/src/models/client.ts +33 -0
  50. package/src/models/consult.ts +110 -0
  51. package/src/models/diagnosis.ts +141 -0
  52. package/src/models/error.ts +13 -0
  53. package/src/models/guard.ts +136 -0
  54. package/src/models/index.ts +9 -0
  55. package/src/models/practice.ts +411 -0
  56. package/src/models/shared.ts +6 -0
  57. package/src/models/vault.ts +158 -0
  58. package/src/models/workflow.ts +142 -0
  59. package/src/sdk-revision/client.ts +62 -0
  60. package/src/sdk-revision/index.ts +1 -0
  61. package/src/services/api.ts +77 -0
  62. package/src/services/axios.ts +91 -0
  63. package/src/services/consult.ts +265 -0
  64. package/src/services/diagnosis.ts +144 -0
  65. package/src/services/external/clinia.ts +133 -0
  66. package/src/services/external/index.ts +1 -0
  67. package/src/services/guard.ts +228 -0
  68. package/src/services/index.ts +10 -0
  69. package/src/services/practice.ts +537 -0
  70. package/src/services/teller.ts +39 -0
  71. package/src/services/vault.ts +178 -0
  72. package/src/services/workflow.ts +36 -0
package/src/client.ts ADDED
@@ -0,0 +1,1843 @@
1
+ import * as OroToolbox from 'oro-toolbox'
2
+ import { CryptoRSA } from 'oro-toolbox'
3
+ import { registerPatient, sessionStorePrivateKeyName, decryptGrants, decryptConsultLockboxGrants} from './helpers'
4
+ import {
5
+ AssociatedLockboxNotFound,
6
+ AuthTokenRequest,
7
+ Consult,
8
+ ConsultRequest,
9
+ DataCreateResponse,
10
+ Document,
11
+ DocumentType,
12
+ EncryptedIndexEntry,
13
+ EncryptedVaultIndex,
14
+ Grant,
15
+ IdentityCreateRequest,
16
+ IdentityResponse,
17
+ IncompleteAuthentication,
18
+ IndexBuildError,
19
+ IndexConsultLockbox,
20
+ IndexKey,
21
+ LocalEncryptedData,
22
+ LocalizedData,
23
+ LockboxDataRequest,
24
+ LockboxGrantRequest,
25
+ LockboxManifest,
26
+ ManifestEntry,
27
+ Meta,
28
+ Metadata,
29
+ MetadataCategory,
30
+ MissingGrant,
31
+ MissingLockbox,
32
+ MissingLockboxOwner,
33
+ PersonalMeta,
34
+ PopulatedWorkflowData,
35
+ Practice,
36
+ PreferenceMeta,
37
+ RecoveryData,
38
+ RecoveryMeta,
39
+ RegisterPatientOutput,
40
+ SecretShard,
41
+ TokenData,
42
+ TosAndCpAcceptanceRequest,
43
+ UserPreference,
44
+ Uuid,
45
+ VaultIndex,
46
+ WorkflowData,
47
+ } from './models'
48
+ import { filterGrantsWithLockboxMetadata, buildLegacyVaultIndex } from './sdk-revision'
49
+ import {
50
+ ConsultService,
51
+ DiagnosisService,
52
+ GuardService,
53
+ PracticeService,
54
+ TellerService,
55
+ VaultService,
56
+ WorkflowService,
57
+ } from './services'
58
+
59
+ export class OroClient {
60
+ private rsa?: CryptoRSA
61
+ private secrets: {
62
+ lockboxUuid: string
63
+ cryptor: OroToolbox.CryptoChaCha
64
+ }[] = []
65
+ private cachedMetadataGrants: {
66
+ [filter: string]: Grant[]
67
+ } = {}
68
+
69
+ private cachedManifest: {
70
+ [filter: string]: ManifestEntry[]
71
+ } = {}
72
+
73
+ private vaultIndex?: VaultIndex
74
+
75
+ constructor(
76
+ private toolbox: typeof OroToolbox,
77
+ public tellerClient: TellerService,
78
+ public vaultClient: VaultService,
79
+ public guardClient: GuardService,
80
+ public practiceClient: PracticeService,
81
+ public consultClient: ConsultService,
82
+ public workflowClient: WorkflowService,
83
+ public diagnosisClient: DiagnosisService,
84
+ private authenticationCallback?: (err: Error) => void
85
+ ) {}
86
+
87
+ /**
88
+ * clears the vaultIndex and cached metadata grants
89
+ */
90
+ public async cleanIndex() {
91
+ this.vaultIndex = undefined
92
+ this.cachedMetadataGrants = {}
93
+ this.cachedManifest = {}
94
+ }
95
+
96
+ /**
97
+ * Generates an RSA key pair and password payload (rsa private key encrypted with the password)
98
+ * Calls Guard to sign up with the email address, password, practice, legal and token data
99
+ *
100
+ * @param email
101
+ * @param password
102
+ * @param practice
103
+ * @param legal
104
+ * @param tokenData
105
+ * @returns
106
+ */
107
+ public async signUp(
108
+ email: string,
109
+ password: string,
110
+ practice: Practice,
111
+ tosAndCpAcceptance: TosAndCpAcceptanceRequest,
112
+ tokenData?: TokenData,
113
+ subscription?: boolean
114
+ ): Promise<IdentityResponse> {
115
+ this.rsa = new CryptoRSA()
116
+ const privateKey = this.rsa.private()
117
+
118
+ const symmetricEncryptor = this.toolbox.CryptoChaCha.fromPassphrase(
119
+ password
120
+ )
121
+ const recoveryPassword = symmetricEncryptor.bytesEncryptToBase64Payload(
122
+ privateKey
123
+ )
124
+
125
+ const hashedPassword = this.toolbox.hashStringToBase64(
126
+ this.toolbox.hashStringToBase64(password)
127
+ )
128
+
129
+ const signupRequest: IdentityCreateRequest = {
130
+ practiceUuid: practice.uuid,
131
+ email: email.toLowerCase(),
132
+ password: hashedPassword,
133
+ publicKey: this.toolbox.encodeToBase64(this.rsa.public()),
134
+ recoveryPassword,
135
+ tosAndCpAcceptance,
136
+ tokenData,
137
+ subscription,
138
+ }
139
+
140
+ const identity = await this.guardClient.identityCreate(signupRequest)
141
+
142
+ if (identity.recoveryLogin) {
143
+ //Ensure we can recover from a page reload
144
+ let symetricEncryptor = this.toolbox.CryptoChaCha.fromPassphrase(
145
+ identity.recoveryLogin
146
+ )
147
+ sessionStorage.setItem(
148
+ sessionStorePrivateKeyName(identity.id),
149
+ symetricEncryptor.bytesEncryptToBase64Payload(privateKey)
150
+ )
151
+ }
152
+
153
+ return identity
154
+ }
155
+
156
+ /**
157
+ * Parse the given accessToken claims by calling guard whoami and update theidentity to set it's emailConfirmed flag
158
+ * @param accessToken
159
+ * @returns The identity related to confirmedEmail
160
+ */
161
+ public async confirmEmail(accessToken: string): Promise<IdentityResponse> {
162
+ this.guardClient.setTokens({ accessToken })
163
+ const claims = await this.guardClient.whoAmI()
164
+ return this.guardClient.identityUpdate(claims.sub, {
165
+ emailConfirmed: true,
166
+ })
167
+ }
168
+
169
+ /**
170
+ * Calls Guard to sign in with the email address, password and one time password (if MFA is enabled)
171
+ * Then recover's the rsa private key from the recovery payload
172
+ *
173
+ * @param practiceUuid
174
+ * @param email
175
+ * @param password
176
+ * @param otp
177
+ * @returns the user identity
178
+ */
179
+ public async signIn(
180
+ practiceUuid: Uuid,
181
+ email: string,
182
+ password: string,
183
+ otp?: string
184
+ ): Promise<IdentityResponse> {
185
+ const hashedPassword = this.toolbox.hashStringToBase64(
186
+ this.toolbox.hashStringToBase64(password)
187
+ )
188
+ const tokenRequest: AuthTokenRequest = {
189
+ practiceUuid,
190
+ email: email.toLowerCase(),
191
+ password: hashedPassword,
192
+ otp,
193
+ }
194
+
195
+ await this.guardClient.authToken(tokenRequest)
196
+ const userUuid = (await this.guardClient.whoAmI()).sub
197
+
198
+ // Updates the rsa key to the one generated on the backend
199
+ await this.recoverPrivateKeyFromPassword(userUuid, password)
200
+ return await this.guardClient.identityGet(userUuid)
201
+ }
202
+
203
+ /**
204
+ * Will attempt to recover an existing login session and set back
205
+ * the private key in scope
206
+ */
207
+ public async resumeSession() {
208
+ const id = (await this.guardClient.whoAmI()).sub
209
+ const recoveryPayload = sessionStorage.getItem(
210
+ sessionStorePrivateKeyName(id)
211
+ )
212
+ const recoveryKey = (await this.guardClient.identityGet(id))
213
+ .recoveryLogin
214
+
215
+ if (!recoveryKey || !recoveryPayload) throw IncompleteAuthentication
216
+
217
+ const symmetricDecryptor = this.toolbox.CryptoChaCha.fromPassphrase(
218
+ recoveryKey
219
+ )
220
+ let privateKey = symmetricDecryptor.base64PayloadDecryptToBytes(
221
+ recoveryPayload
222
+ )
223
+ this.rsa = this.toolbox.CryptoRSA.fromKey(privateKey)
224
+ }
225
+
226
+ /**
227
+ * This function let's you encrypt locally an Object
228
+ * @param value the Object to encrypt
229
+ * @returns a LocalEncryptedData Object
230
+ * @throws IncompleteAuthentication if rsa is not set
231
+ * @calls authenticationCallback if rsa is not set
232
+ */
233
+ public localEncryptToJsonPayload(value: any): LocalEncryptedData {
234
+ if (!this.rsa) {
235
+ if (this.authenticationCallback) {
236
+ this.authenticationCallback(new IncompleteAuthentication())
237
+ }
238
+
239
+ throw new IncompleteAuthentication()
240
+ }
241
+
242
+ const chaChaKey = new this.toolbox.CryptoChaCha()
243
+
244
+ const encryptedData = chaChaKey.jsonEncryptToBase64Payload(value)
245
+ const encryptedKey = this.toolbox.encodeToBase64(
246
+ this.rsa.encryptToBytes(chaChaKey.key())
247
+ )
248
+
249
+ return { encryptedData, encryptedKey }
250
+ }
251
+
252
+ /**
253
+ * This function let's you decrypt a LocalEncryptedData object
254
+ * @param value a LocalEncryptedData object
255
+ * @returns a decrypted Object
256
+ * @throws IncompleteAuthentication if rsa is not set
257
+ * @calls authenticationCallback if rsa is not set
258
+ */
259
+ public localDecryptJsonPayload({
260
+ encryptedKey,
261
+ encryptedData,
262
+ }: LocalEncryptedData): any {
263
+ if (!this.rsa) {
264
+ if (this.authenticationCallback) {
265
+ this.authenticationCallback(new IncompleteAuthentication())
266
+ }
267
+
268
+ throw new IncompleteAuthentication()
269
+ }
270
+
271
+ const chaChaKey = this.rsa.base64DecryptToBytes(encryptedKey)
272
+ const decryptedData = this.toolbox.CryptoChaCha.fromKey(
273
+ chaChaKey
274
+ ).base64PayloadDecryptToJson(encryptedData)
275
+
276
+ return decryptedData
277
+ }
278
+
279
+ /**
280
+ * Effectively kills your "session"
281
+ */
282
+ public async signOut() {
283
+ this.rsa = undefined
284
+ this.secrets = []
285
+ this.guardClient.setTokens({
286
+ accessToken: undefined,
287
+ refreshToken: undefined,
288
+ })
289
+ await this.guardClient.authLogout()
290
+ }
291
+
292
+ /**
293
+ * @name registerPatient
294
+ * @description The complete flow to register a patient
295
+ *
296
+ * Steps:
297
+ * 1. Create a consult (checks if payment has been done)
298
+ * 2. Creates a lockbox
299
+ * 3. Grants lockbox access to all practice personnel
300
+ * 4. Creates secure identification, medical, onboarding data
301
+ * 5. Generates and stores the rsa key pair and recovery payloads
302
+ *
303
+ * @param patientUuid
304
+ * @param consult
305
+ * @param workflow
306
+ * @param recoveryQA
307
+ * @returns
308
+ */
309
+ public async registerPatient(
310
+ patientUuid: Uuid,
311
+ consult: ConsultRequest,
312
+ workflow: WorkflowData,
313
+ recoveryQA?: {
314
+ recoverySecurityQuestions: string[]
315
+ recoverySecurityAnswers: string[]
316
+ }
317
+ ): Promise<RegisterPatientOutput> {
318
+ if (!this.rsa) throw IncompleteAuthentication
319
+ return registerPatient(
320
+ patientUuid,
321
+ consult,
322
+ workflow,
323
+ this,
324
+ this.toolbox.uuid(),
325
+ recoveryQA
326
+ )
327
+ }
328
+
329
+ /**
330
+ * Builds the vault index for the logged user
331
+ *
332
+ * Steps:
333
+ * 1. Retrieves, decrypts and sets the lockbox IndexSnapshot
334
+ * 2. Retrieves, decrypts and adds all other index entries starting at the snapshot timestamp
335
+ * 3. Updates the IndexSnapshot if changed
336
+ * @deprecated
337
+ * @returns the latest vault index
338
+ */
339
+ public async buildVaultIndex(forceRefresh: boolean = false) {
340
+ if (!this.vaultIndex || forceRefresh)
341
+ await buildLegacyVaultIndex(this)
342
+ }
343
+
344
+ /**
345
+ * Setter for the vault index
346
+ * @param index
347
+ */
348
+ public setVaultIndex(index: VaultIndex) {
349
+ this.vaultIndex = index
350
+ }
351
+
352
+ /**
353
+ * Fetches all grants, and consultations that exist in each lockbox
354
+ * Then updates the index for the current user with the lockbox consult relationship
355
+ */
356
+ public async forceUpdateIndexEntries() {
357
+ let grants = await this.getGrants()
358
+
359
+ let indexConsultLockbox: IndexConsultLockbox[] = await Promise.all(
360
+ grants.map(
361
+ async (grant: Grant) =>
362
+ await this.vaultClient
363
+ .lockboxMetadataGet(
364
+ grant.lockboxUuid!,
365
+ ['consultationId'],
366
+ [],
367
+ { category: MetadataCategory.Consultation },
368
+ grant.lockboxOwnerUuid
369
+ )
370
+ .then((consults) => {
371
+ try {
372
+ return consults[0].map((consult: any) => {
373
+ return {
374
+ ...consult,
375
+ grant: {
376
+ lockboxOwnerUuid:
377
+ grant.lockboxOwnerUuid,
378
+ lockboxUuid: grant.lockboxUuid,
379
+ },
380
+ }
381
+ })
382
+ } catch (e) {
383
+ // No consultations in lockbox or index could not be created
384
+ return []
385
+ }
386
+ })
387
+ .catch(() => [])
388
+ )
389
+ ).then((consults) => consults.flat())
390
+ this.vaultIndexAdd({
391
+ [IndexKey.Consultation]: indexConsultLockbox,
392
+ })
393
+ .then(() => alert('The Index was successfully updated!'))
394
+ .catch(() => console.error('The index failed to update!'))
395
+ }
396
+
397
+ /**
398
+ * Generates, encrypts and adds entries to vault index for a given index owner
399
+ *
400
+ * @param entries
401
+ * @param indexOwnerUuid
402
+ */
403
+ public async vaultIndexAdd(entries: VaultIndex, indexOwnerUuid?: Uuid) {
404
+ if (!this.rsa) throw IncompleteAuthentication
405
+
406
+ let rsaPub: Uint8Array
407
+ if (indexOwnerUuid) {
408
+ let base64IndexOwnerPubKey = (
409
+ await this.guardClient.identityGet(indexOwnerUuid)
410
+ ).publicKey
411
+ rsaPub = this.toolbox.decodeFromBase64(base64IndexOwnerPubKey)
412
+ } else {
413
+ rsaPub = this.rsa.public()
414
+ }
415
+
416
+ let encryptedIndex: EncryptedVaultIndex = {}
417
+
418
+ for (let keyString of Object.keys(entries)) {
419
+ let key = keyString as keyof VaultIndex
420
+ switch (key) {
421
+ case IndexKey.ConsultationLockbox:
422
+ encryptedIndex[key] = (entries[
423
+ key
424
+ ] as IndexConsultLockbox[])
425
+ .map((e) => ({
426
+ ...e,
427
+ uniqueHash: e.consultationId,
428
+ }))
429
+ .map(
430
+ (e: IndexConsultLockbox) =>
431
+ ({
432
+ uuid: e.uuid,
433
+ timestamp: e.timestamp,
434
+ uniqueHash: e.uniqueHash,
435
+ encryptedIndexEntry: CryptoRSA.jsonWithPubEncryptToBase64(
436
+ {
437
+ consultationId: e.consultationId,
438
+ grant: e.grant,
439
+ },
440
+ rsaPub
441
+ ),
442
+ } as EncryptedIndexEntry)
443
+ )
444
+ break
445
+ //// DEPRECATED : REMOVE ME : BEGIN ///////////////////////////////////////////
446
+ case IndexKey.Consultation:
447
+ encryptedIndex[key] = (entries[
448
+ key
449
+ ] as IndexConsultLockbox[])
450
+ .map((e) => ({
451
+ ...e,
452
+ uniqueHash: this.toolbox.hashStringToBase64(
453
+ JSON.stringify({
454
+ consultationId: e.consultationId,
455
+ grant: e.grant,
456
+ })
457
+ ),
458
+ }))
459
+ .filter(
460
+ (e) =>
461
+ !this.vaultIndex ||
462
+ !this.vaultIndex[IndexKey.Consultation]?.find(
463
+ (v) => v.uniqueHash === e.uniqueHash
464
+ )
465
+ )
466
+ .map(
467
+ (e: IndexConsultLockbox) =>
468
+ ({
469
+ uuid: e.uuid,
470
+ timestamp: e.timestamp,
471
+ uniqueHash: e.uniqueHash,
472
+ encryptedIndexEntry: CryptoRSA.jsonWithPubEncryptToBase64(
473
+ {
474
+ consultationId: e.consultationId,
475
+ grant: e.grant,
476
+ },
477
+ rsaPub
478
+ ),
479
+ } as EncryptedIndexEntry)
480
+ )
481
+ break
482
+ //// DEPRECATED : REMOVE ME : END ///////////////////////////////////////////
483
+ }
484
+ }
485
+ await this.vaultClient.vaultIndexPut(encryptedIndex, indexOwnerUuid)
486
+ }
487
+
488
+ /**
489
+ * adds or updates the index snapshot for the logged user
490
+ * @param index
491
+ */
492
+ public async indexSnapshotAdd(index: VaultIndex) {
493
+ if (!this.rsa) throw IncompleteAuthentication
494
+ let rsaPub: Uint8Array = this.rsa.public()
495
+
496
+ let cleanedIndex: VaultIndex = {
497
+ [IndexKey.Consultation]: index[IndexKey.Consultation]
498
+ ?.filter((c) => c)
499
+ .map((c) => {
500
+ return {
501
+ grant: c.grant,
502
+ consultationId: c.consultationId,
503
+ }
504
+ }),
505
+ }
506
+
507
+ // the data of the snapshot should not contain the `IndexEntry` data
508
+ // (will create conflicts while updating)
509
+ let encryptedIndexEntry = CryptoRSA.jsonWithPubEncryptToBase64(
510
+ cleanedIndex,
511
+ rsaPub
512
+ )
513
+
514
+ // The encryptedIndexEntry can have the uuid and timstamp (for updating)
515
+ let encryptedIndex: EncryptedIndexEntry = {
516
+ uuid: index.uuid,
517
+ timestamp: index.timestamp,
518
+ encryptedIndexEntry,
519
+ }
520
+ this.vaultClient.vaultIndexSnapshotPut(encryptedIndex)
521
+ }
522
+
523
+ /**
524
+ * @name grantLockbox
525
+ * @description Grants a lockbox by retrieving the shared secret of the lockbox and encrypting it with the grantees public key
526
+ * @param granteeUuid
527
+ * @param lockboxUuid
528
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
529
+ */
530
+ public async grantLockbox(
531
+ granteeUuid: Uuid,
532
+ lockboxUuid: Uuid,
533
+ lockboxOwnerUuid?: Uuid
534
+ ) {
535
+ if (!this.rsa) throw IncompleteAuthentication
536
+
537
+ let secret = (
538
+ await this.getCachedSecretCryptor(lockboxUuid, lockboxOwnerUuid)
539
+ ).key()
540
+ let base64GranteePublicKey = (
541
+ await this.guardClient.identityGet(granteeUuid)
542
+ ).publicKey
543
+ let granteePublicKey = this.toolbox.decodeFromBase64(
544
+ base64GranteePublicKey
545
+ )
546
+
547
+ let granteeEncryptedSecret = CryptoRSA.bytesWithPubEncryptToBase64(
548
+ secret,
549
+ granteePublicKey
550
+ )
551
+ let request: LockboxGrantRequest = {
552
+ encryptedSecret: granteeEncryptedSecret,
553
+ granteeUuid: granteeUuid,
554
+ }
555
+ await this.vaultClient.lockboxGrant(
556
+ lockboxUuid,
557
+ request,
558
+ lockboxOwnerUuid
559
+ )
560
+ }
561
+
562
+ /**
563
+ * @name createMessageData
564
+ * @description Creates a Base64 encrypted Payload to send and store in the vault from a message string
565
+ * @param lockboxUuid
566
+ * @param message
567
+ * @param consultationId the consultation for which this message is sent
568
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
569
+ * @param previousDataUuid if it's a revision of existing file, specify the previous data uuid
570
+ * @returns the data uuid
571
+ */
572
+ public async createMessageData(
573
+ lockboxUuid: Uuid,
574
+ message: string,
575
+ consultationId: string,
576
+ lockboxOwnerUuid?: Uuid,
577
+ previousDataUuid?: Uuid
578
+ ): Promise<DataCreateResponse> {
579
+ if (!this.rsa) throw IncompleteAuthentication
580
+
581
+ let symmetricEncryptor = await this.getCachedSecretCryptor(
582
+ lockboxUuid,
583
+ lockboxOwnerUuid
584
+ )
585
+
586
+ let encryptedData = symmetricEncryptor.jsonEncryptToBase64Payload(
587
+ message
588
+ )
589
+ let encryptedPrivateMeta = symmetricEncryptor.jsonEncryptToBase64Payload(
590
+ { author: (await this.guardClient.whoAmI()).sub }
591
+ )
592
+
593
+ let meta = {
594
+ consultationId,
595
+ category: MetadataCategory.Consultation,
596
+ documentType: DocumentType.Message,
597
+ contentType: 'text/plain',
598
+ }
599
+
600
+ let request: LockboxDataRequest = {
601
+ data: encryptedData,
602
+ publicMetadata: meta,
603
+ privateMetadata: encryptedPrivateMeta,
604
+ }
605
+
606
+ return this.tellerClient.lockboxDataStore(
607
+ lockboxUuid,
608
+ request,
609
+ lockboxOwnerUuid,
610
+ previousDataUuid
611
+ )
612
+ }
613
+
614
+ /**
615
+ * @name createMessageAttachmentData
616
+ * @description Creates a Base64 encrypted Payload to send and store in the vault from a file
617
+ * @param lockboxUuid
618
+ * @param data the file stored
619
+ * @param consultationId the consultation for which this message is sent
620
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
621
+ * @param previousDataUuid if it's a revision of existing file, specify the previous data uuid
622
+ * @returns the data uuid
623
+ */
624
+ public async createMessageAttachmentData(
625
+ lockboxUuid: Uuid,
626
+ data: File,
627
+ consultationId: string,
628
+ lockboxOwnerUuid?: Uuid,
629
+ previousDataUuid?: Uuid
630
+ ): Promise<DataCreateResponse> {
631
+ if (!this.rsa) throw IncompleteAuthentication
632
+
633
+ let symmetricEncryptor = await this.getCachedSecretCryptor(
634
+ lockboxUuid,
635
+ lockboxOwnerUuid
636
+ )
637
+ let encryptedData = symmetricEncryptor.bytesEncryptToBase64Payload(
638
+ new Uint8Array(await data.arrayBuffer())
639
+ )
640
+ let encryptedPrivateMeta = symmetricEncryptor.jsonEncryptToBase64Payload(
641
+ {
642
+ author: (await this.guardClient.whoAmI()).sub,
643
+ fileName: data.name,
644
+ lastModified: data.lastModified,
645
+ size: data.size,
646
+ }
647
+ )
648
+
649
+ let meta = {
650
+ consultationId,
651
+ category: MetadataCategory.Consultation,
652
+ documentType: DocumentType.Message,
653
+ contentType: data.type,
654
+ }
655
+
656
+ let request: LockboxDataRequest = {
657
+ data: encryptedData,
658
+ publicMetadata: meta,
659
+ privateMetadata: encryptedPrivateMeta,
660
+ }
661
+
662
+ return this.tellerClient.lockboxDataStore(
663
+ lockboxUuid,
664
+ request,
665
+ lockboxOwnerUuid,
666
+ previousDataUuid
667
+ )
668
+ }
669
+
670
+ /**
671
+ * @name createAttachmentData
672
+ * @description Creates a Base64 encrypted Payload to send and store in the vault from a file
673
+ * @param lockboxUuid
674
+ * @param data the file stored
675
+ * @param consultationId the consultation for which this message is sent
676
+ * @param category the category for the attachment data
677
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
678
+ * @param previousDataUuid if it's a revision of existing file, specify the previous data uuid
679
+ * @returns the data uuid
680
+ */
681
+ public async createConsultationAttachmentData(
682
+ lockboxUuid: Uuid,
683
+ data: File,
684
+ consultationId: string,
685
+ documentType: DocumentType,
686
+ lockboxOwnerUuid?: Uuid,
687
+ previousDataUuid?: Uuid
688
+ ): Promise<DataCreateResponse> {
689
+ if (!this.rsa) throw IncompleteAuthentication
690
+
691
+ return this.createBytesData<Meta | any>(
692
+ lockboxUuid,
693
+ new Uint8Array(await data.arrayBuffer()),
694
+ {
695
+ consultationId,
696
+ category: MetadataCategory.Consultation,
697
+ documentType,
698
+ contentType: data.type,
699
+ },
700
+ {
701
+ author: (await this.guardClient.whoAmI()).sub,
702
+ fileName: data.name,
703
+ },
704
+ lockboxOwnerUuid,
705
+ previousDataUuid
706
+ )
707
+ }
708
+
709
+ /**
710
+ * @name createJsonData
711
+ * @description Creates a Base64 encrypted Payload to send and store in the vault. With the data input as a JSON
712
+ * @param lockboxUuid
713
+ * @param data
714
+ * @param meta
715
+ * @param privateMeta the metadata that will be secured in the vault
716
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
717
+ * @param previousDataUuid if it's a revision of existing data, specify the previous data uuid
718
+ * @returns the data uuid
719
+ */
720
+ public async createJsonData<T = Meta>(
721
+ lockboxUuid: Uuid,
722
+ data: any,
723
+ meta?: T,
724
+ privateMeta?: { [val: string]: any },
725
+ lockboxOwnerUuid?: Uuid,
726
+ previousDataUuid?: Uuid
727
+ ): Promise<DataCreateResponse> {
728
+ if (!this.rsa) throw IncompleteAuthentication
729
+
730
+ let symmetricEncryptor = await this.getCachedSecretCryptor(
731
+ lockboxUuid,
732
+ lockboxOwnerUuid
733
+ )
734
+ let encryptedData = symmetricEncryptor.jsonEncryptToBase64Payload(data)
735
+ let encryptedPrivateMeta = symmetricEncryptor.jsonEncryptToBase64Payload(
736
+ privateMeta
737
+ )
738
+
739
+ let request: LockboxDataRequest = {
740
+ data: encryptedData,
741
+ publicMetadata: meta,
742
+ privateMetadata: encryptedPrivateMeta,
743
+ }
744
+
745
+ return this.tellerClient.lockboxDataStore(
746
+ lockboxUuid,
747
+ request,
748
+ lockboxOwnerUuid,
749
+ previousDataUuid
750
+ )
751
+ }
752
+
753
+ /**
754
+ * Get or upsert a data in lockbox
755
+ * @param lockboxUuid the lockbox uuid
756
+ * @param data the data to insert
757
+ * @param publicMetadata the public Metadata
758
+ * @param privateMetadata the private Metadata
759
+ * @param forceReplace set true when the insertion of data requires to replace the data when it exists already
760
+ * @returns the data uuid
761
+ */
762
+ public async getOrInsertJsonData<M = Metadata>(
763
+ lockboxUuid: Uuid,
764
+ data: any,
765
+ publicMetadata: M,
766
+ privateMetadata: Metadata,
767
+ forceReplace: boolean = false
768
+ ): Promise<Uuid> {
769
+ let manifest = await this.vaultClient.lockboxManifestGet(
770
+ lockboxUuid,
771
+ publicMetadata
772
+ )
773
+ if (!forceReplace && manifest.length > 0) {
774
+ console.log(
775
+ `The data for ${JSON.stringify(publicMetadata)} already exist`
776
+ )
777
+ return manifest[0].dataUuid
778
+ } else
779
+ return (
780
+ await this.createJsonData<M>(
781
+ lockboxUuid,
782
+ data,
783
+ publicMetadata,
784
+ privateMetadata,
785
+ undefined,
786
+ forceReplace && manifest.length > 0
787
+ ? manifest[0].dataUuid
788
+ : undefined // if forceReplace and data already exist, then replace data. Otherwise insert it
789
+ ).catch((err) => {
790
+ console.error(
791
+ `Error while upserting data ${JSON.stringify(
792
+ publicMetadata
793
+ )} data`,
794
+ err
795
+ )
796
+ throw err
797
+ })
798
+ ).dataUuid
799
+ }
800
+
801
+ /**
802
+ * @name createBytesData
803
+ * @description Creates a Base64 encrypted Payload to send and store in the vault. With the data input as a Bytes
804
+ * @param lockboxUuid
805
+ * @param data
806
+ * @param meta
807
+ * @param privateMeta the metadata that will be secured in the vault
808
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
809
+ * @param previousDataUuid if it's a revision of existing data, specify the previous data uuid
810
+ * @returns the data uuid
811
+ */
812
+ public async createBytesData<T = Meta>(
813
+ lockboxUuid: Uuid,
814
+ data: Uint8Array,
815
+ meta: T,
816
+ privateMeta: { [val: string]: any },
817
+ lockboxOwnerUuid?: Uuid,
818
+ previousDataUuid?: Uuid
819
+ ): Promise<DataCreateResponse> {
820
+ if (!this.rsa) throw IncompleteAuthentication
821
+ let symmetricEncryptor = await this.getCachedSecretCryptor(
822
+ lockboxUuid,
823
+ lockboxOwnerUuid
824
+ )
825
+ let encryptedData = symmetricEncryptor.bytesEncryptToBase64Payload(data)
826
+ let encryptedPrivateMeta = symmetricEncryptor.jsonEncryptToBase64Payload(
827
+ privateMeta
828
+ )
829
+
830
+ let request: LockboxDataRequest = {
831
+ data: encryptedData,
832
+ publicMetadata: meta,
833
+ privateMetadata: encryptedPrivateMeta,
834
+ }
835
+
836
+ return this.tellerClient.lockboxDataStore(
837
+ lockboxUuid,
838
+ request,
839
+ lockboxOwnerUuid,
840
+ previousDataUuid
841
+ )
842
+ }
843
+
844
+ /**
845
+ * @name getJsonData
846
+ * @description Fetches and decrypts the lockbox data with the cached shared secret.
847
+ * Decrypts the data to a valid JSON object. If this is impossible, the call to the WASM binary will fail
848
+ *
849
+ * @type T is the generic type specifying the return type object of the function
850
+ * @param lockboxUuid
851
+ * @param dataUuid
852
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
853
+ * @returns the data specified by the generic type <T>
854
+ */
855
+ public async getJsonData<T = any>(
856
+ lockboxUuid: Uuid,
857
+ dataUuid: Uuid,
858
+ lockboxOwnerUuid?: Uuid
859
+ ): Promise<T> {
860
+ if (!this.rsa) throw IncompleteAuthentication
861
+
862
+ let [encryptedPayload, symmetricDecryptor] = await Promise.all([
863
+ this.vaultClient.lockboxDataGet(
864
+ lockboxUuid,
865
+ dataUuid,
866
+ lockboxOwnerUuid
867
+ ),
868
+ this.getCachedSecretCryptor(lockboxUuid, lockboxOwnerUuid),
869
+ ])
870
+
871
+ return symmetricDecryptor.base64PayloadDecryptToJson(
872
+ encryptedPayload.data
873
+ )
874
+ }
875
+ /**
876
+ * @description Fetches and decrypts the lockbox data with the cached shared secret.
877
+ * @param lockboxUuid
878
+ * @param dataUuid
879
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
880
+ * @returns the bytes data
881
+ */
882
+ public async getBytesData(
883
+ lockboxUuid: Uuid,
884
+ dataUuid: Uuid,
885
+ lockboxOwnerUuid?: Uuid
886
+ ): Promise<Uint8Array> {
887
+ if (!this.rsa) throw IncompleteAuthentication
888
+
889
+ let [encryptedPayload, symmetricDecryptor] = await Promise.all([
890
+ this.vaultClient.lockboxDataGet(
891
+ lockboxUuid,
892
+ dataUuid,
893
+ lockboxOwnerUuid
894
+ ),
895
+ this.getCachedSecretCryptor(lockboxUuid, lockboxOwnerUuid),
896
+ ])
897
+
898
+ return symmetricDecryptor.base64PayloadDecryptToBytes(
899
+ encryptedPayload.data
900
+ )
901
+ }
902
+
903
+ /**
904
+ * @name getGrants
905
+ * @description Get all lockboxes granted to user with the applied filter
906
+ * @note this function returns cached grants and will not update unless the page is refreshed
907
+ * @todo some versions of lockboxes do not make use of lockbox metadata
908
+ * in this case, all lockboxes need to be filtered one-by-one to find the correct one
909
+ * Remove if this is no longer the case
910
+ * @param filter: the consultationId in which the grant exists
911
+ * @returns decrypted lockboxes granted to user
912
+ */
913
+ public async getGrants(
914
+ filter?: { consultationId: Uuid },
915
+ forceRefresh: boolean = false
916
+ ): Promise<Grant[]> {
917
+ if (!this.rsa) throw IncompleteAuthentication
918
+
919
+ let filterString = JSON.stringify(filter)
920
+ // retrieves cached grants
921
+ // Note: if filters is set to empty, it will be stored in the `undefined` key
922
+ if (!forceRefresh && this.cachedMetadataGrants[filterString])
923
+ return this.cachedMetadataGrants[filterString]
924
+
925
+ // retrieves the consult lockbox from the vault directly if it exists
926
+ // Note: will work only if the filter being applied is exclusively a consult id
927
+ const grantsByConsultLockbox = (await this.vaultClient.vaultIndexGet([IndexKey.ConsultationLockbox], [filter?.consultationId!]))[IndexKey.ConsultationLockbox]
928
+ const decryptedConsults = decryptConsultLockboxGrants(grantsByConsultLockbox ?? [], this.rsa)
929
+ if (decryptedConsults.length > 0) {
930
+ console.info('[sdk:index] Grants found in user`s constant time secure index')
931
+ this.cachedMetadataGrants[JSON.stringify(filter)] = decryptedConsults
932
+ return this.cachedMetadataGrants[filterString]
933
+ }
934
+
935
+ let encryptedGrants
936
+ // if there are no grants with the applied filter from index, attempt for naive filter with backwards compatibility
937
+ if (filter) {
938
+ encryptedGrants = await filterGrantsWithLockboxMetadata(
939
+ this,
940
+ filter,
941
+ this.vaultIndex,
942
+ forceRefresh
943
+ )
944
+ } else {
945
+ encryptedGrants = (await this.vaultClient.grantsGet()).grants
946
+ }
947
+
948
+ const decryptedGrants = await decryptGrants(encryptedGrants, this.rsa)
949
+ // sets the cached grant
950
+ this.cachedMetadataGrants[filterString] = decryptedGrants
951
+ return decryptedGrants
952
+ }
953
+
954
+ /**
955
+ * @name getCachedSecretCryptor
956
+ * @description Retrieves the cached lockbox secret or fetches the secret from vault, then creates the symmetric cryptor and stores it in memory
957
+ * @param lockboxUuid
958
+ * @param lockboxOwnerUuid the lockbox owner (ignored if lockbox is owned by self)
959
+ * @returns
960
+ */
961
+ async getCachedSecretCryptor(
962
+ lockboxUuid: string,
963
+ lockboxOwnerUuid?: string
964
+ ): Promise<OroToolbox.CryptoChaCha> {
965
+ if (!this.rsa) throw IncompleteAuthentication
966
+
967
+ let index = this.secrets.findIndex(
968
+ (secret) => secret.lockboxUuid === lockboxUuid
969
+ )
970
+ if (index === -1) {
971
+ let encryptedSecret = (
972
+ await this.vaultClient.lockboxSecretGet(
973
+ lockboxUuid,
974
+ lockboxOwnerUuid
975
+ )
976
+ ).sharedSecret
977
+
978
+ let secret = this.rsa.base64DecryptToBytes(encryptedSecret)
979
+ let cryptor = this.toolbox.CryptoChaCha.fromKey(secret)
980
+ this.secrets.push({ lockboxUuid, cryptor })
981
+ return cryptor
982
+ } else {
983
+ return this.secrets[index].cryptor
984
+ }
985
+ }
986
+
987
+ /**
988
+ * Retrieves the patient personal information associated to the `consultationId`
989
+ * The `consultationId` only helps to retrieve the patient lockboxes
990
+ * Note: it is possible to have several personal informations data
991
+ * @param consultationId The consultation Id
992
+ * @param category The personal MetadataCategory to fetch
993
+ * @param forceRefresh force data refresh (default to false)
994
+ * @returns the personal data
995
+ */
996
+ public async getPersonalInformationsFromConsultId(
997
+ consultationId: Uuid,
998
+ category:
999
+ | MetadataCategory.Personal
1000
+ | MetadataCategory.ChildPersonal
1001
+ | MetadataCategory.OtherPersonal,
1002
+ forceRefresh = false
1003
+ ): Promise<LocalizedData<PopulatedWorkflowData>[]> {
1004
+ return this.getMetaCategoryFromConsultId(
1005
+ consultationId,
1006
+ category,
1007
+ forceRefresh
1008
+ )
1009
+ }
1010
+
1011
+ /**
1012
+ * Retrieves the patient medical data associated to the `consultationId`
1013
+ * The `consultationId` only helps to retrieve the patient lockboxes
1014
+ * Note: it is possible to have several medical data
1015
+ * @param consultationId The consultation Id
1016
+ * @param forceRefresh force data refresh (default to false)
1017
+ * @returns the medical data
1018
+ */
1019
+ public async getMedicalDataFromConsultId(
1020
+ consultationId: Uuid,
1021
+ forceRefresh = false
1022
+ ): Promise<LocalizedData<PopulatedWorkflowData>[]> {
1023
+ return this.getMetaCategoryFromConsultId(
1024
+ consultationId,
1025
+ MetadataCategory.Medical,
1026
+ forceRefresh
1027
+ )
1028
+ }
1029
+
1030
+ private async getMetaCategoryFromConsultId(
1031
+ consultationId: Uuid,
1032
+ category: MetadataCategory,
1033
+ forceRefresh = false
1034
+ ): Promise<LocalizedData<PopulatedWorkflowData>[]> {
1035
+ let grants = await this.getGrants({ consultationId })
1036
+ let workflowData: LocalizedData<PopulatedWorkflowData>[] = []
1037
+ for (let grant of grants) {
1038
+ let manifest = await this.getLockboxManifest(
1039
+ grant.lockboxUuid!,
1040
+ {
1041
+ category,
1042
+ documentType: DocumentType.PopulatedWorkflowData,
1043
+ consultationIds: [consultationId],
1044
+ },
1045
+ true,
1046
+ grant.lockboxOwnerUuid,
1047
+ forceRefresh
1048
+ )
1049
+
1050
+ // TODO: find another solution for backwards compatibility (those without the metadata consultationIds)
1051
+ if (manifest.length === 0) {
1052
+ manifest = (
1053
+ await this.getLockboxManifest(
1054
+ grant.lockboxUuid!,
1055
+ {
1056
+ category,
1057
+ documentType: DocumentType.PopulatedWorkflowData,
1058
+ // backward compatiblility with TonTest
1059
+ },
1060
+ true,
1061
+ grant.lockboxOwnerUuid,
1062
+ forceRefresh
1063
+ )
1064
+ ).filter((entry) => !entry.metadata.consultationIds) // Keep only entries without associated consultationIds
1065
+ }
1066
+ let data = await Promise.all(
1067
+ manifest.map(async (entry) => {
1068
+ return {
1069
+ lockboxOwnerUuid: grant.lockboxOwnerUuid,
1070
+ lockboxUuid: grant.lockboxUuid!,
1071
+ dataUuid: entry.dataUuid,
1072
+ data: await this.getJsonData<PopulatedWorkflowData>(
1073
+ grant.lockboxUuid!,
1074
+ entry.dataUuid
1075
+ ),
1076
+ }
1077
+ })
1078
+ )
1079
+ workflowData = { ...workflowData, ...data }
1080
+ }
1081
+ return workflowData
1082
+ }
1083
+
1084
+ /**
1085
+ * @description retrieves the personal information stored in the first owned lockbox
1086
+ * @param userId The user Id
1087
+ * @returns the personal data
1088
+ */
1089
+ public async getPersonalInformations(
1090
+ userId: Uuid
1091
+ ): Promise<LocalizedData<PopulatedWorkflowData>> {
1092
+ const grant = (await this.getGrants()).find(
1093
+ (lockbox) => lockbox.lockboxOwnerUuid === userId
1094
+ )
1095
+
1096
+ if (!grant) {
1097
+ throw MissingGrant
1098
+ }
1099
+
1100
+ const { lockboxUuid, lockboxOwnerUuid } = grant
1101
+
1102
+ if (!lockboxUuid) throw MissingLockbox
1103
+
1104
+ if (!lockboxOwnerUuid) throw MissingLockboxOwner
1105
+
1106
+ const identificationDataUuid = (
1107
+ await this.getLockboxManifest(
1108
+ lockboxUuid,
1109
+ {
1110
+ category: MetadataCategory.Personal,
1111
+ documentType: DocumentType.PopulatedWorkflowData,
1112
+ },
1113
+ false,
1114
+ userId
1115
+ )
1116
+ )[0].dataUuid
1117
+
1118
+ return {
1119
+ lockboxOwnerUuid,
1120
+ lockboxUuid,
1121
+ dataUuid: identificationDataUuid,
1122
+ data: await this.getJsonData<PopulatedWorkflowData>(
1123
+ lockboxUuid,
1124
+ identificationDataUuid
1125
+ ),
1126
+ }
1127
+ }
1128
+
1129
+ /**
1130
+ * Retrieves the grant associated to a consultationId
1131
+ * @note returns the first grant only
1132
+ * @param consultationId The consultationId
1133
+ * @returns the grant
1134
+ */
1135
+ public async getGrantFromConsultId(
1136
+ consultationId: Uuid
1137
+ ): Promise<Grant | undefined> {
1138
+ let grants = await this.getGrants({ consultationId })
1139
+
1140
+ if (grants.length === 0) {
1141
+ throw AssociatedLockboxNotFound
1142
+ }
1143
+
1144
+ return grants[0]
1145
+ }
1146
+
1147
+ /**
1148
+ * retrieves the identity associated to the `consultationId`
1149
+ * @param consultationId The consultation Id
1150
+ * @returns the identity
1151
+ */
1152
+ public async getIdentityFromConsultId(
1153
+ consultationId: Uuid
1154
+ ): Promise<IdentityResponse | undefined> {
1155
+ const grant = await this.getGrantFromConsultId(consultationId)
1156
+
1157
+ if (grant && grant.lockboxOwnerUuid) {
1158
+ return await this.guardClient.identityGet(grant.lockboxOwnerUuid)
1159
+ } else {
1160
+ return undefined
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * retrieves the lockbox manifest for a given lockbox and add's its private metadata
1166
+ * @note the lockbox manifest will retrieved the cached manifest first unless force refresh is enabled
1167
+ * @param lockboxUuid
1168
+ * @param filter
1169
+ * @param expandPrivateMetadata
1170
+ * @param lockboxOwnerUuid
1171
+ * @param forceRefresh
1172
+ * @returns the lockbox manifest
1173
+ */
1174
+ public async getLockboxManifest(
1175
+ lockboxUuid: Uuid,
1176
+ filter: Metadata,
1177
+ expandPrivateMetadata: boolean,
1178
+ lockboxOwnerUuid?: Uuid,
1179
+ forceRefresh: boolean = false
1180
+ ): Promise<LockboxManifest> {
1181
+ let manifestKey = JSON.stringify({
1182
+ lockboxUuid,
1183
+ filter,
1184
+ expandPrivateMetadata,
1185
+ lockboxOwnerUuid,
1186
+ })
1187
+ if (!forceRefresh && this.cachedManifest[manifestKey])
1188
+ return this.cachedManifest[manifestKey]
1189
+
1190
+ return this.vaultClient
1191
+ .lockboxManifestGet(lockboxUuid, filter, lockboxOwnerUuid)
1192
+ .then((manifest) => {
1193
+ return Promise.all(
1194
+ manifest.map(async (entry) => {
1195
+ if (
1196
+ expandPrivateMetadata &&
1197
+ entry.metadata.privateMetadata
1198
+ ) {
1199
+ let privateMeta = await this.getJsonData<Metadata>(
1200
+ lockboxUuid!,
1201
+ entry.metadata.privateMetadata,
1202
+ lockboxOwnerUuid
1203
+ )
1204
+ entry.metadata = {
1205
+ ...entry.metadata,
1206
+ ...privateMeta,
1207
+ }
1208
+ }
1209
+ return entry
1210
+ })
1211
+ ).then(
1212
+ (manifest) => (this.cachedManifest[manifestKey] = manifest)
1213
+ )
1214
+ })
1215
+ }
1216
+
1217
+ /**
1218
+ * @description Create or update the personal information and store it in the first owned lockbox
1219
+ * @param identity The identity to use
1220
+ * @param data The personal data to store
1221
+ * @param dataUuid (optional) The dataUuid to update
1222
+ * @returns
1223
+ */
1224
+ public async createPersonalInformations(
1225
+ identity: IdentityResponse,
1226
+ data: PopulatedWorkflowData,
1227
+ dataUuid?: string
1228
+ ): Promise<DataCreateResponse> {
1229
+ const lockboxUuid = (await this.getGrants()).find(
1230
+ (lockbox) => lockbox.lockboxOwnerUuid === identity.id
1231
+ )?.lockboxUuid
1232
+
1233
+ if (lockboxUuid) {
1234
+ return this.createJsonData<PersonalMeta>(
1235
+ lockboxUuid,
1236
+ data,
1237
+ {
1238
+ category: MetadataCategory.Personal,
1239
+ documentType: DocumentType.PopulatedWorkflowData,
1240
+ },
1241
+ {},
1242
+ undefined,
1243
+ dataUuid
1244
+ )
1245
+ } else {
1246
+ throw MissingLockbox
1247
+ }
1248
+ }
1249
+
1250
+ /**
1251
+ * Create or update user Preference
1252
+ * @param identity
1253
+ * @param preference
1254
+ * @param dataUuid
1255
+ * @returns
1256
+ */
1257
+ public async createUserPreference(
1258
+ identity: IdentityResponse,
1259
+ preference: UserPreference,
1260
+ dataUuid?: string
1261
+ ): Promise<DataCreateResponse> {
1262
+ const lockboxUuid = (await this.getGrants()).find(
1263
+ (lockbox) => lockbox.lockboxOwnerUuid === identity.id
1264
+ )?.lockboxUuid
1265
+
1266
+ if (lockboxUuid) {
1267
+ return this.createJsonData<PreferenceMeta>(
1268
+ lockboxUuid,
1269
+ preference,
1270
+ {
1271
+ category: MetadataCategory.Preference,
1272
+ contentType: 'application/json',
1273
+ },
1274
+ {},
1275
+ undefined,
1276
+ dataUuid
1277
+ )
1278
+ } else {
1279
+ throw MissingLockbox
1280
+ }
1281
+ }
1282
+
1283
+ /**
1284
+ * retrieves the user preference from a grant
1285
+ * @param grant The grant
1286
+ * @returns the user preference
1287
+ */
1288
+ public async getDataFromGrant<T = any>(
1289
+ grant: Grant,
1290
+ filter: Metadata
1291
+ ): Promise<LocalizedData<T>> {
1292
+ const { lockboxUuid, lockboxOwnerUuid } = grant
1293
+
1294
+ if (!lockboxUuid) throw MissingLockbox
1295
+ if (!lockboxOwnerUuid) throw MissingLockboxOwner
1296
+ const identificationDataUuid = (
1297
+ await this.getLockboxManifest(
1298
+ lockboxUuid,
1299
+
1300
+ filter,
1301
+ false,
1302
+ grant.lockboxOwnerUuid,
1303
+ true
1304
+ )
1305
+ )[0].dataUuid
1306
+
1307
+ return {
1308
+ lockboxOwnerUuid,
1309
+ lockboxUuid,
1310
+ dataUuid: identificationDataUuid,
1311
+ data: await this.getJsonData<T>(
1312
+ lockboxUuid,
1313
+ identificationDataUuid
1314
+ ),
1315
+ }
1316
+ }
1317
+
1318
+ /**
1319
+ * retrieves the user preference from a consultation id
1320
+ * @param consultationId The related consultationId
1321
+ * @returns the user preference
1322
+ */
1323
+ public async getUserPreferenceFromConsultId(
1324
+ consultationId: string
1325
+ ): Promise<LocalizedData<UserPreference>> {
1326
+ const grant = await this.getGrantFromConsultId(consultationId)
1327
+
1328
+ if (!grant) throw MissingGrant
1329
+
1330
+ return this.getDataFromGrant<UserPreference>(grant, {
1331
+ category: MetadataCategory.Preference,
1332
+ contentType: 'application/json',
1333
+ })
1334
+ }
1335
+
1336
+ /**
1337
+ * retrieves the user preference stored in the first owned lockbox from identity
1338
+ * @param identity The identity to use
1339
+ * @returns the user preference
1340
+ */
1341
+ public async getUserPreference(
1342
+ identity: IdentityResponse
1343
+ ): Promise<LocalizedData<UserPreference>> {
1344
+ const grant = (await this.getGrants()).find(
1345
+ (lockbox) => lockbox.lockboxOwnerUuid === identity.id
1346
+ )
1347
+
1348
+ if (!grant) throw MissingGrant
1349
+
1350
+ return this.getDataFromGrant<UserPreference>(grant, {
1351
+ category: MetadataCategory.Preference,
1352
+ contentType: 'application/json',
1353
+ })
1354
+ }
1355
+
1356
+ /**
1357
+ * retrieves the user preference from a consultation id
1358
+ * @param consultationId The related consultationId
1359
+ * @returns the user preference
1360
+ */
1361
+ public async getRecoveryDataFromConsultId(
1362
+ consultationId: string
1363
+ ): Promise<LocalizedData<RecoveryData>> {
1364
+ const grant = await this.getGrantFromConsultId(consultationId)
1365
+
1366
+ if (!grant) throw MissingGrant
1367
+
1368
+ return this.getDataFromGrant<RecoveryData>(grant, {
1369
+ category: MetadataCategory.Recovery,
1370
+ contentType: 'application/json',
1371
+ })
1372
+ }
1373
+
1374
+ /**
1375
+ * retrieves the user preference stored in the first owned lockbox from identity
1376
+ * @param identity The identity to use
1377
+ * @returns the user preference
1378
+ */
1379
+ public async getRecoveryData(
1380
+ identity: IdentityResponse
1381
+ ): Promise<LocalizedData<RecoveryData>> {
1382
+ const grant = (await this.getGrants()).find(
1383
+ (lockbox) => lockbox.lockboxOwnerUuid === identity.id
1384
+ )
1385
+
1386
+ if (!grant) throw MissingGrant
1387
+
1388
+ return this.getDataFromGrant(grant, {
1389
+ category: MetadataCategory.Recovery,
1390
+ contentType: 'application/json',
1391
+ })
1392
+ }
1393
+
1394
+ /**
1395
+ * @name getAssignedConsultations
1396
+ * @description finds all assigned or owned consultations for the logged user
1397
+ * Steps:
1398
+ * - Retrieves all granted lockboxes given to the logged user
1399
+ * - for each lockbox, find all consultation ids
1400
+ * - for each consultation id, retrieve the consult information
1401
+ * @param practiceUuid the uuid of the practice to look consult into
1402
+ * @returns the list of consults
1403
+ */
1404
+ public async getAssignedConsultations(
1405
+ practiceUuid: Uuid,
1406
+ forceRefresh: boolean = false
1407
+ ): Promise<Consult[]> {
1408
+ return Promise.all(
1409
+ (await this.getGrants(undefined, forceRefresh)).map((grant) =>
1410
+ this.getLockboxManifest(
1411
+ grant.lockboxUuid!,
1412
+ {
1413
+ category: MetadataCategory.Consultation,
1414
+ documentType: DocumentType.PopulatedWorkflowData,
1415
+ },
1416
+ true,
1417
+ undefined,
1418
+ forceRefresh
1419
+ ).then((manifest) =>
1420
+ Promise.all(
1421
+ manifest.map(
1422
+ async (entry) =>
1423
+ await this.consultClient.getConsultByUUID(
1424
+ entry.metadata.consultationId,
1425
+ practiceUuid
1426
+ )
1427
+ )
1428
+ ).then((promise) => promise.flat())
1429
+ )
1430
+ )
1431
+ ).then((consults) => consults.flat())
1432
+ }
1433
+
1434
+ /**
1435
+ * Gets the past consultations of the patient as well as his relatives if any
1436
+ * @param consultationId any consultation uuid from which we will fetch all the other consultations of the same patient as the owner of this consultation id
1437
+ * @param practiceUuid
1438
+ */
1439
+ public async getPastConsultationsFromConsultId(
1440
+ consultationId: string,
1441
+ practiceUuid: string
1442
+ ): Promise<Consult[] | undefined> {
1443
+ const grant = await this.getGrantFromConsultId(consultationId)
1444
+ if (!grant) return undefined
1445
+
1446
+ let consultationsInLockbox: string[] = (
1447
+ await this.vaultClient.lockboxMetadataGet(
1448
+ grant.lockboxUuid!,
1449
+ ['consultationId'],
1450
+ ['consultationId'],
1451
+ {
1452
+ category: MetadataCategory.Consultation,
1453
+ documentType: DocumentType.PopulatedWorkflowData,
1454
+ },
1455
+ grant.lockboxOwnerUuid
1456
+ )
1457
+ )
1458
+ .flat()
1459
+ .map(
1460
+ (metadata: { consultationId: string }) =>
1461
+ metadata.consultationId
1462
+ )
1463
+
1464
+ if (consultationsInLockbox.length == 0) return []
1465
+
1466
+ return await Promise.all(
1467
+ consultationsInLockbox.map(async (consultId: string) => {
1468
+ return await this.consultClient.getConsultByUUID(
1469
+ consultId,
1470
+ practiceUuid
1471
+ )
1472
+ })
1473
+ )
1474
+ }
1475
+
1476
+ /**
1477
+ * @name getPatientConsultationData
1478
+ * @description retrieves the consultation data
1479
+ * @param consultationId
1480
+ * @returns
1481
+ */
1482
+ public async getPatientConsultationData(
1483
+ consultationId: Uuid,
1484
+ forceRefresh: boolean = false
1485
+ ): Promise<PopulatedWorkflowData[]> {
1486
+ //TODO: make use of getPatientDocumentsList instead of doing it manually here
1487
+ return Promise.all(
1488
+ (await this.getGrants({ consultationId }, forceRefresh))
1489
+ .map((grant) =>
1490
+ this.getLockboxManifest(
1491
+ grant.lockboxUuid!,
1492
+ {
1493
+ category: MetadataCategory.Consultation,
1494
+ documentType: DocumentType.PopulatedWorkflowData,
1495
+ consultationId, //since we want to update the cached manifest (if another consult data exists)
1496
+ },
1497
+ true,
1498
+ grant.lockboxOwnerUuid,
1499
+ forceRefresh
1500
+ ).then((manifest) =>
1501
+ Promise.all(
1502
+ manifest.map((e) =>
1503
+ this.getJsonData<PopulatedWorkflowData>(
1504
+ grant.lockboxUuid!,
1505
+ e.dataUuid,
1506
+ grant.lockboxOwnerUuid
1507
+ )
1508
+ )
1509
+ )
1510
+ )
1511
+ )
1512
+ .flat()
1513
+ ).then((data) => data.flat())
1514
+ }
1515
+
1516
+ /**
1517
+ * This function returns the patient prescriptions
1518
+ * @param consultationId
1519
+ * @returns
1520
+ */
1521
+ public async getPatientPrescriptionsList(
1522
+ consultationId: Uuid
1523
+ ): Promise<Document[]> {
1524
+ return this.getPatientDocumentsList(
1525
+ {
1526
+ category: MetadataCategory.Consultation,
1527
+ documentType: DocumentType.Prescription,
1528
+ },
1529
+ true,
1530
+ consultationId
1531
+ )
1532
+ }
1533
+
1534
+ /**
1535
+ * This function returns the patient results
1536
+ * @param consultationId
1537
+ * @returns
1538
+ */
1539
+ public async getPatientResultsList(
1540
+ consultationId: Uuid
1541
+ ): Promise<Document[]> {
1542
+ return this.getPatientDocumentsList(
1543
+ {
1544
+ category: MetadataCategory.Consultation,
1545
+ documentType: DocumentType.Result,
1546
+ },
1547
+ true,
1548
+ consultationId
1549
+ )
1550
+ }
1551
+
1552
+ /**
1553
+ * returns the patient treatment plan options
1554
+ * @param consultationId
1555
+ * @returns Document[] corresponding to the patient treatment plan options
1556
+ */
1557
+ public async getPatientTreatmentPlans(
1558
+ consultationId: Uuid
1559
+ ): Promise<Document[]> {
1560
+ return this.getPatientDocumentsList(
1561
+ {
1562
+ category: MetadataCategory.Consultation,
1563
+ documentType: DocumentType.TreatmentPlan,
1564
+ },
1565
+ true,
1566
+ consultationId
1567
+ )
1568
+ }
1569
+
1570
+ /**
1571
+ * returns a specific patient treatment plan option
1572
+ * @param consultationId
1573
+ * @param treatmentPlanId
1574
+ * @returns
1575
+ */
1576
+ public async getPatientTreatmentPlanByUuid(
1577
+ consultationId: Uuid,
1578
+ treatmentPlanId: Uuid
1579
+ ): Promise<Document[]> {
1580
+ return this.getPatientDocumentsList(
1581
+ {
1582
+ category: MetadataCategory.Consultation,
1583
+ documentType: DocumentType.TreatmentPlan,
1584
+ treatmentPlanId,
1585
+ },
1586
+ true,
1587
+ consultationId
1588
+ )
1589
+ }
1590
+
1591
+ /**
1592
+ * @name getPatientDocumentsList
1593
+ * @description applies the provided filter to the vault to only find those documents
1594
+ * @param filters the applied filters (e.g. type of documents)
1595
+ * @param expandPrivateMetadata whether or not, the private metadata needs to be retrieved
1596
+ * (more computationally expensive)
1597
+ * @param consultationId
1598
+ * @returns the filtered document list
1599
+ */
1600
+ public async getPatientDocumentsList(
1601
+ filters: Object,
1602
+ expandPrivateMetadata: boolean,
1603
+ consultationId: Uuid
1604
+ ): Promise<Document[]> {
1605
+ return Promise.all(
1606
+ (await this.getGrants({ consultationId }))
1607
+ .map((grant) =>
1608
+ this.getLockboxManifest(
1609
+ grant.lockboxUuid!,
1610
+ { ...filters, consultationId },
1611
+ expandPrivateMetadata,
1612
+ grant.lockboxOwnerUuid,
1613
+ true
1614
+ ).then((manifest) =>
1615
+ Promise.all(
1616
+ manifest.map(
1617
+ async (entry): Promise<Document> => {
1618
+ return {
1619
+ lockboxOwnerUuid:
1620
+ grant.lockboxOwnerUuid,
1621
+ lockboxUuid: grant.lockboxUuid!,
1622
+ ...entry,
1623
+ }
1624
+ }
1625
+ )
1626
+ )
1627
+ )
1628
+ )
1629
+ .flat()
1630
+ ).then((data) => data.flat())
1631
+ }
1632
+
1633
+ /****************************************************************************************************************
1634
+ * RECOVERY *
1635
+ ****************************************************************************************************************/
1636
+
1637
+ /**
1638
+ * @name recoverPrivateKeyFromSecurityQuestions
1639
+ * @description Recovers and sets the rsa private key from the answered security questions
1640
+ * @param id
1641
+ * @param recoverySecurityQuestions
1642
+ * @param recoverySecurityAnswers
1643
+ * @param threshold the number of answers needed to recover the key
1644
+ */
1645
+ public async recoverPrivateKeyFromSecurityQuestions(
1646
+ id: Uuid,
1647
+ recoverySecurityQuestions: string[],
1648
+ recoverySecurityAnswers: string[],
1649
+ threshold: number
1650
+ ) {
1651
+ let shards: SecretShard[] = (await this.guardClient.identityGet(id))
1652
+ .recoverySecurityQuestions
1653
+ let answeredShards = shards
1654
+ .filter((shard: any) => {
1655
+ // filters all answered security questions
1656
+ let indexOfQuestion = recoverySecurityQuestions.indexOf(
1657
+ shard.securityQuestion
1658
+ )
1659
+ if (indexOfQuestion === -1) return false
1660
+ return (
1661
+ recoverySecurityAnswers[indexOfQuestion] &&
1662
+ recoverySecurityAnswers[indexOfQuestion] != ''
1663
+ )
1664
+ })
1665
+ .map((item: any) => {
1666
+ // appends the security answer to the answered shards
1667
+ let index = recoverySecurityQuestions.indexOf(
1668
+ item.securityQuestion
1669
+ )
1670
+ item.securityAnswer = recoverySecurityAnswers[index]
1671
+ return item
1672
+ })
1673
+ try {
1674
+ // reconstructs the key from the answered security answers
1675
+ let privateKey = this.toolbox.reconstructSecret(
1676
+ answeredShards,
1677
+ threshold
1678
+ )
1679
+ this.rsa = this.toolbox.CryptoRSA.fromKey(privateKey)
1680
+ } catch (e) {
1681
+ console.error(e)
1682
+ }
1683
+ }
1684
+
1685
+ /**
1686
+ * @name recoverPrivateKeyFromPassword
1687
+ * @description Recovers and sets the rsa private key from the password
1688
+ * @param id
1689
+ * @param password
1690
+ */
1691
+ public async recoverPrivateKeyFromPassword(id: Uuid, password: string) {
1692
+ let identity = await this.guardClient.identityGet(id)
1693
+
1694
+ let recoveryPayload = identity.recoveryPassword
1695
+ let symmetricDecryptor = this.toolbox.CryptoChaCha.fromPassphrase(
1696
+ password
1697
+ )
1698
+ let privateKey = symmetricDecryptor.base64PayloadDecryptToBytes(
1699
+ recoveryPayload
1700
+ )
1701
+
1702
+ if (identity.recoveryLogin) {
1703
+ //Ensure we can recover from a page reload
1704
+ let symetricEncryptor = this.toolbox.CryptoChaCha.fromPassphrase(
1705
+ identity.recoveryLogin
1706
+ )
1707
+ sessionStorage.setItem(
1708
+ sessionStorePrivateKeyName(id),
1709
+ symetricEncryptor.bytesEncryptToBase64Payload(privateKey)
1710
+ )
1711
+ }
1712
+
1713
+ this.rsa = this.toolbox.CryptoRSA.fromKey(privateKey)
1714
+ }
1715
+
1716
+ /**
1717
+ * @name recoverPrivateKeyFromMasterKey
1718
+ * @description Recovers and sets the rsa private key from the master key
1719
+ * @param id
1720
+ * @param masterKey
1721
+ */
1722
+ public async recoverPrivateKeyFromMasterKey(id: Uuid, masterKey: string) {
1723
+ let recoveryPayload = (await this.guardClient.identityGet(id))
1724
+ .recoveryMasterKey
1725
+ let symmetricDecryptor = this.toolbox.CryptoChaCha.fromPassphrase(
1726
+ masterKey
1727
+ )
1728
+ let privateKey = symmetricDecryptor.base64PayloadDecryptToBytes(
1729
+ recoveryPayload
1730
+ )
1731
+ this.rsa = this.toolbox.CryptoRSA.fromKey(privateKey)
1732
+ }
1733
+
1734
+ /**
1735
+ * @description Generates and updates the security questions and answers payload using new recovery questions and answers
1736
+ * Important: Since the security questions generate a payload for the private key, they will never be stored on the device as they must remain secret!!!
1737
+ * @param id
1738
+ * @param recoverySecurityQuestions
1739
+ * @param recoverySecurityAnswers
1740
+ * @param threshold the number of answers needed to rebuild the secret
1741
+ */
1742
+ public async updateSecurityQuestions(
1743
+ id: Uuid,
1744
+ recoverySecurityQuestions: string[],
1745
+ recoverySecurityAnswers: string[],
1746
+ threshold: number
1747
+ ) {
1748
+ if (!this.rsa) throw IncompleteAuthentication
1749
+ let securityQuestionPayload = this.toolbox.breakSecretIntoShards(
1750
+ recoverySecurityQuestions,
1751
+ recoverySecurityAnswers,
1752
+ this.rsa.private(),
1753
+ threshold
1754
+ )
1755
+ let updateRequest = {
1756
+ recoverySecurityQuestions: securityQuestionPayload,
1757
+ }
1758
+
1759
+ return await this.guardClient.identityUpdate(id, updateRequest)
1760
+ }
1761
+
1762
+ /**
1763
+ * @description Generates and stores the payload encrypted payload and updates the password itself (double hash)
1764
+ * @important
1765
+ * the recovery payload uses a singly hashed password and the password stored is doubly hashed so
1766
+ * the stored password cannot derive the decryption key in the payload
1767
+ * @note
1768
+ * the old password must be provided when not performing an account recovery
1769
+ * @param id
1770
+ * @param newPassword
1771
+ * @param oldPassword
1772
+ */
1773
+ public async updatePassword(
1774
+ id: Uuid,
1775
+ newPassword: string,
1776
+ oldPassword?: string
1777
+ ) {
1778
+ if (!this.rsa) throw IncompleteAuthentication
1779
+
1780
+ let symmetricEncryptor = this.toolbox.CryptoChaCha.fromPassphrase(
1781
+ newPassword
1782
+ )
1783
+ let passwordPayload = symmetricEncryptor.bytesEncryptToBase64Payload(
1784
+ this.rsa.private()
1785
+ )
1786
+ if (oldPassword) {
1787
+ oldPassword = this.toolbox.hashStringToBase64(
1788
+ this.toolbox.hashStringToBase64(oldPassword)
1789
+ )
1790
+ }
1791
+
1792
+ newPassword = this.toolbox.hashStringToBase64(
1793
+ this.toolbox.hashStringToBase64(newPassword)
1794
+ )
1795
+
1796
+ let updateRequest = {
1797
+ password: {
1798
+ oldPassword,
1799
+ newPassword,
1800
+ },
1801
+ recoveryPassword: passwordPayload,
1802
+ }
1803
+
1804
+ return await this.guardClient.identityUpdate(id, updateRequest)
1805
+ }
1806
+
1807
+ /**
1808
+ * @description Generates and stores the master key encrypted payload
1809
+ * Important
1810
+ * Since the master key is used to generate a payload for the private key, it will never be stored on the device as it must remain secret!
1811
+ * @param id
1812
+ * @param masterKey
1813
+ * @param lockboxUuid
1814
+ */
1815
+ async updateMasterKey(id: Uuid, masterKey: string, lockboxUuid: Uuid) {
1816
+ if (!this.rsa) throw IncompleteAuthentication
1817
+
1818
+ let symmetricEncryptor = this.toolbox.CryptoChaCha.fromPassphrase(
1819
+ masterKey
1820
+ )
1821
+ let masterKeyPayload = symmetricEncryptor.bytesEncryptToBase64Payload(
1822
+ this.rsa.private()
1823
+ )
1824
+ let updateRequest = { recoveryMasterKey: masterKeyPayload }
1825
+ const updatedIdentity = await this.guardClient.identityUpdate(
1826
+ id,
1827
+ updateRequest
1828
+ )
1829
+
1830
+ await this.getOrInsertJsonData<RecoveryMeta>(
1831
+ lockboxUuid,
1832
+ { masterKey },
1833
+ {
1834
+ category: MetadataCategory.Recovery,
1835
+ contentType: 'application/json',
1836
+ },
1837
+ {},
1838
+ true
1839
+ )
1840
+
1841
+ return updatedIdentity
1842
+ }
1843
+ }