@bifold/oca 1.0.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 (165) hide show
  1. package/README.md +33 -0
  2. package/build/__tests__/remote.test.d.ts +1 -0
  3. package/build/__tests__/remote.test.js +162 -0
  4. package/build/constants.d.ts +4 -0
  5. package/build/constants.js +7 -0
  6. package/build/formatters/credential/CredentialFormatter.d.ts +8 -0
  7. package/build/formatters/credential/CredentialFormatter.js +32 -0
  8. package/build/formatters/credential/DisplayAttribute.d.ts +11 -0
  9. package/build/formatters/credential/DisplayAttribute.js +18 -0
  10. package/build/formatters/credential/LocalizedCredential.d.ts +13 -0
  11. package/build/formatters/credential/LocalizedCredential.js +57 -0
  12. package/build/formatters/credential/index.d.ts +4 -0
  13. package/build/formatters/credential/index.js +12 -0
  14. package/build/formatters/index.d.ts +4 -0
  15. package/build/formatters/index.js +12 -0
  16. package/build/index.d.ts +5 -0
  17. package/build/index.js +34 -0
  18. package/build/interfaces/data/base/BaseOverlayData.interface.d.ts +5 -0
  19. package/build/interfaces/data/base/BaseOverlayData.interface.js +2 -0
  20. package/build/interfaces/data/branding/BrandingOverlayData.interface.d.ts +12 -0
  21. package/build/interfaces/data/branding/BrandingOverlayData.interface.js +2 -0
  22. package/build/interfaces/data/branding/LegacyBrandingOverlayData.interface.d.ts +15 -0
  23. package/build/interfaces/data/branding/LegacyBrandingOverlayData.interface.js +2 -0
  24. package/build/interfaces/data/bundle/OverlayBundleData.interface.d.ts +6 -0
  25. package/build/interfaces/data/bundle/OverlayBundleData.interface.js +2 -0
  26. package/build/interfaces/data/capture-base/CaptureBaseData.interface.d.ts +7 -0
  27. package/build/interfaces/data/capture-base/CaptureBaseData.interface.js +2 -0
  28. package/build/interfaces/data/index.d.ts +12 -0
  29. package/build/interfaces/data/index.js +2 -0
  30. package/build/interfaces/data/semantic/CharacterEncodingOverlayData.interface.d.ts +6 -0
  31. package/build/interfaces/data/semantic/CharacterEncodingOverlayData.interface.js +2 -0
  32. package/build/interfaces/data/semantic/FormatOverlayData.interface.d.ts +4 -0
  33. package/build/interfaces/data/semantic/FormatOverlayData.interface.js +2 -0
  34. package/build/interfaces/data/semantic/InformationOverlayData.interface.d.ts +5 -0
  35. package/build/interfaces/data/semantic/InformationOverlayData.interface.js +2 -0
  36. package/build/interfaces/data/semantic/LabelOverlayData.interface.d.ts +7 -0
  37. package/build/interfaces/data/semantic/LabelOverlayData.interface.js +2 -0
  38. package/build/interfaces/data/semantic/MetaOverlayData.interface.d.ts +12 -0
  39. package/build/interfaces/data/semantic/MetaOverlayData.interface.js +2 -0
  40. package/build/interfaces/data/semantic/StandardOverlayData.interface.d.ts +5 -0
  41. package/build/interfaces/data/semantic/StandardOverlayData.interface.js +2 -0
  42. package/build/interfaces/index.d.ts +2 -0
  43. package/build/interfaces/index.js +18 -0
  44. package/build/interfaces/overlay/OverlayBundleAttribute.interface.d.ts +9 -0
  45. package/build/interfaces/overlay/OverlayBundleAttribute.interface.js +2 -0
  46. package/build/interfaces/overlay/OverlayBundleMetadata.interface.d.ts +9 -0
  47. package/build/interfaces/overlay/OverlayBundleMetadata.interface.js +2 -0
  48. package/build/interfaces/overlay/index.d.ts +3 -0
  49. package/build/interfaces/overlay/index.js +2 -0
  50. package/build/legacy/index.d.ts +3 -0
  51. package/build/legacy/index.js +19 -0
  52. package/build/legacy/resolver/oca.d.ts +99 -0
  53. package/build/legacy/resolver/oca.js +239 -0
  54. package/build/legacy/resolver/record.d.ts +50 -0
  55. package/build/legacy/resolver/record.js +37 -0
  56. package/build/legacy/resolver/remote-oca.d.ts +186 -0
  57. package/build/legacy/resolver/remote-oca.js +536 -0
  58. package/build/types/OverlayTypeMap.d.ts +5 -0
  59. package/build/types/OverlayTypeMap.js +25 -0
  60. package/build/types/TypeEnums.d.ts +19 -0
  61. package/build/types/TypeEnums.js +24 -0
  62. package/build/types/base/BaseOverlay.d.ts +8 -0
  63. package/build/types/base/BaseOverlay.js +28 -0
  64. package/build/types/branding/BrandingOverlay.d.ts +15 -0
  65. package/build/types/branding/BrandingOverlay.js +68 -0
  66. package/build/types/branding/LegacyBrandingOverlay.d.ts +18 -0
  67. package/build/types/branding/LegacyBrandingOverlay.js +51 -0
  68. package/build/types/bundle/OverlayBundle.d.ts +20 -0
  69. package/build/types/bundle/OverlayBundle.js +167 -0
  70. package/build/types/capture-base/CaptureBase.d.ts +10 -0
  71. package/build/types/capture-base/CaptureBase.js +30 -0
  72. package/build/types/index.d.ts +14 -0
  73. package/build/types/index.js +33 -0
  74. package/build/types/semantic/CharacterEncodingOverlay.d.ts +9 -0
  75. package/build/types/semantic/CharacterEncodingOverlay.js +43 -0
  76. package/build/types/semantic/FormatOverlay.d.ts +7 -0
  77. package/build/types/semantic/FormatOverlay.js +30 -0
  78. package/build/types/semantic/InformationOverlay.d.ts +8 -0
  79. package/build/types/semantic/InformationOverlay.js +31 -0
  80. package/build/types/semantic/LabelOverlay.d.ts +10 -0
  81. package/build/types/semantic/LabelOverlay.js +41 -0
  82. package/build/types/semantic/MetaOverlay.d.ts +15 -0
  83. package/build/types/semantic/MetaOverlay.js +54 -0
  84. package/build/types/semantic/StandardOverlay.d.ts +8 -0
  85. package/build/types/semantic/StandardOverlay.js +38 -0
  86. package/build/utils/color/generateColor.d.ts +2 -0
  87. package/build/utils/color/generateColor.js +11 -0
  88. package/build/utils/color/hashCode.d.ts +8 -0
  89. package/build/utils/color/hashCode.js +12 -0
  90. package/build/utils/color/hashToRGBA.d.ts +8 -0
  91. package/build/utils/color/hashToRGBA.js +23 -0
  92. package/build/utils/color/index.d.ts +5 -0
  93. package/build/utils/color/index.js +15 -0
  94. package/build/utils/color/luminanceForHexColor.d.ts +7 -0
  95. package/build/utils/color/luminanceForHexColor.js +18 -0
  96. package/build/utils/color/mulberry32.d.ts +9 -0
  97. package/build/utils/color/mulberry32.js +16 -0
  98. package/build/utils/color/textColorForBackground.d.ts +1 -0
  99. package/build/utils/color/textColorForBackground.js +24 -0
  100. package/build/utils/credential-definition.d.ts +1 -0
  101. package/build/utils/credential-definition.js +21 -0
  102. package/build/utils/index.d.ts +1 -0
  103. package/build/utils/index.js +17 -0
  104. package/build/utils/logger.d.ts +5 -0
  105. package/build/utils/logger.js +17 -0
  106. package/build/utils/schema.d.ts +4 -0
  107. package/build/utils/schema.js +19 -0
  108. package/package.json +53 -0
  109. package/src/__tests__/__snapshots__/remote.test.ts.snap +293 -0
  110. package/src/__tests__/fixtures/bundle.json +131 -0
  111. package/src/__tests__/fixtures/oca.json +4 -0
  112. package/src/__tests__/fixtures/ocabundles.json +142 -0
  113. package/src/__tests__/remote.test.ts +180 -0
  114. package/src/constants.ts +7 -0
  115. package/src/formatters/credential/CredentialFormatter.ts +20 -0
  116. package/src/formatters/credential/DisplayAttribute.ts +29 -0
  117. package/src/formatters/credential/LocalizedCredential.ts +53 -0
  118. package/src/formatters/credential/index.ts +5 -0
  119. package/src/formatters/index.ts +5 -0
  120. package/src/index.ts +5 -0
  121. package/src/interfaces/data/base/BaseOverlayData.interface.ts +5 -0
  122. package/src/interfaces/data/branding/BrandingOverlayData.interface.ts +13 -0
  123. package/src/interfaces/data/branding/LegacyBrandingOverlayData.interface.ts +16 -0
  124. package/src/interfaces/data/bundle/OverlayBundleData.interface.ts +7 -0
  125. package/src/interfaces/data/capture-base/CaptureBaseData.interface.ts +7 -0
  126. package/src/interfaces/data/index.ts +25 -0
  127. package/src/interfaces/data/semantic/CharacterEncodingOverlayData.interface.ts +8 -0
  128. package/src/interfaces/data/semantic/FormatOverlayData.interface.ts +5 -0
  129. package/src/interfaces/data/semantic/InformationOverlayData.interface.ts +6 -0
  130. package/src/interfaces/data/semantic/LabelOverlayData.interface.ts +8 -0
  131. package/src/interfaces/data/semantic/MetaOverlayData.interface.ts +13 -0
  132. package/src/interfaces/data/semantic/StandardOverlayData.interface.ts +7 -0
  133. package/src/interfaces/index.ts +2 -0
  134. package/src/interfaces/overlay/OverlayBundleAttribute.interface.ts +9 -0
  135. package/src/interfaces/overlay/OverlayBundleMetadata.interface.ts +9 -0
  136. package/src/interfaces/overlay/index.ts +4 -0
  137. package/src/legacy/index.ts +3 -0
  138. package/src/legacy/resolver/oca.ts +377 -0
  139. package/src/legacy/resolver/record.ts +81 -0
  140. package/src/legacy/resolver/remote-oca.ts +661 -0
  141. package/src/types/OverlayTypeMap.ts +25 -0
  142. package/src/types/TypeEnums.ts +20 -0
  143. package/src/types/base/BaseOverlay.ts +18 -0
  144. package/src/types/branding/BrandingOverlay.ts +61 -0
  145. package/src/types/branding/LegacyBrandingOverlay.ts +47 -0
  146. package/src/types/bundle/OverlayBundle.ts +203 -0
  147. package/src/types/capture-base/CaptureBase.ts +22 -0
  148. package/src/types/index.ts +30 -0
  149. package/src/types/semantic/CharacterEncodingOverlay.ts +30 -0
  150. package/src/types/semantic/FormatOverlay.ts +15 -0
  151. package/src/types/semantic/InformationOverlay.ts +18 -0
  152. package/src/types/semantic/LabelOverlay.ts +30 -0
  153. package/src/types/semantic/MetaOverlay.ts +48 -0
  154. package/src/types/semantic/StandardOverlay.ts +24 -0
  155. package/src/utils/color/generateColor.ts +8 -0
  156. package/src/utils/color/hashCode.ts +11 -0
  157. package/src/utils/color/hashToRGBA.ts +22 -0
  158. package/src/utils/color/index.ts +7 -0
  159. package/src/utils/color/luminanceForHexColor.ts +19 -0
  160. package/src/utils/color/mulberry32.ts +15 -0
  161. package/src/utils/color/textColorForBackground.ts +18 -0
  162. package/src/utils/credential-definition.ts +19 -0
  163. package/src/utils/index.ts +1 -0
  164. package/src/utils/logger.ts +14 -0
  165. package/src/utils/schema.ts +16 -0
@@ -0,0 +1,661 @@
1
+ import { getUnQualifiedDidIndyDid } from '@credo-ts/anoncreds'
2
+ import axios from 'axios'
3
+ import { CachesDirectoryPath, readFile, writeFile, exists, mkdir, unlink } from 'react-native-fs'
4
+
5
+ import {
6
+ ocaBundleStorageDirectory,
7
+ ocaCacheDataFileName,
8
+ defaultBundleIndexFileName,
9
+ defaultBundleLanguage,
10
+ } from '../../constants'
11
+ import { IOverlayBundleData } from '../../interfaces'
12
+ import { BaseOverlay, BrandingOverlay, LegacyBrandingOverlay, OverlayBundle } from '../../types'
13
+ import { generateColor } from '../../utils'
14
+
15
+ import { BrandingOverlayType, DefaultOCABundleResolver, Identifiers, OCABundle, OCABundleResolverOptions } from './oca'
16
+
17
+ export interface RemoteOCABundleResolverOptions extends OCABundleResolverOptions {
18
+ indexFileName?: string
19
+ verifyCacheIntegrity?: boolean
20
+ }
21
+
22
+ type BundleIndexEntry = {
23
+ path: string
24
+ sha256: string
25
+ }
26
+
27
+ type BundleIndex = {
28
+ [key: string]: BundleIndexEntry
29
+ }
30
+
31
+ enum OCABundleQueueEntryOperation {
32
+ Add = 'add',
33
+ Remove = 'remove',
34
+ }
35
+
36
+ type OCABundleQueueEntry = {
37
+ sha256: string
38
+ operation: OCABundleQueueEntryOperation
39
+ }
40
+
41
+ type CacheDataFile = {
42
+ indexFileEtag: string
43
+
44
+ updatedAt: Date
45
+ }
46
+
47
+ export class RemoteOCABundleResolver extends DefaultOCABundleResolver {
48
+ protected indexFile: BundleIndex = {}
49
+ private axiosInstance: axios.AxiosInstance
50
+ private cacheDataFileName = ocaCacheDataFileName
51
+ private indexFileName: string
52
+ private _indexFileEtag?: string
53
+
54
+ constructor(indexFileBaseUrl: string, options?: RemoteOCABundleResolverOptions) {
55
+ super({}, options)
56
+
57
+ this.indexFileName = options?.indexFileName || defaultBundleIndexFileName
58
+ this.axiosInstance = axios.create({
59
+ baseURL: indexFileBaseUrl,
60
+ })
61
+ }
62
+
63
+ /**
64
+ * Sets the value of the index file ETag.
65
+ *
66
+ * @param value - The new value for the index file ETag.
67
+ */
68
+ set indexFileEtag(value: string) {
69
+ if (!value) {
70
+ return
71
+ }
72
+
73
+ this._indexFileEtag = value
74
+ this.saveCacheData({
75
+ indexFileEtag: value,
76
+ updatedAt: new Date(),
77
+ }).catch((error) => {
78
+ this.log?.error(`Failed to save cache data, ${error}`)
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Gets the ETag of the index file.
84
+ *
85
+ * @returns The ETag of the index file, or an empty string if not available.
86
+ */
87
+ get indexFileEtag(): string {
88
+ return this._indexFileEtag || ''
89
+ }
90
+
91
+ /**
92
+ * Checks for updates in the OCA (Overlay Capture Architecture) index.
93
+ * If the index file ETag is not available, it loads the cache data and retrieves the ETag from it.
94
+ * Then, it loads the OCA index.
95
+ * @returns A promise that resolves when the update check is complete.
96
+ */
97
+ public async checkForUpdates(): Promise<void> {
98
+ await this.createWorkingDirectoryIfNotExists()
99
+
100
+ if (!this.indexFileEtag) {
101
+ this.log?.info('Loading cache data')
102
+
103
+ const cacheData = await this.loadCacheData()
104
+ if (cacheData) {
105
+ this.indexFileEtag = cacheData.indexFileEtag
106
+ }
107
+ }
108
+
109
+ this.log?.info('Loading OCA index now')
110
+ await this.loadOCAIndex(this.indexFileName)
111
+ }
112
+
113
+ /**
114
+ * Loads the cache data from the local storage.
115
+ * @returns A promise that resolves to the cache data file, or undefined if the cache file does not exist or cannot be loaded.
116
+ */
117
+ private loadCacheData = async (): Promise<CacheDataFile | undefined> => {
118
+ const cacheFileExists = await this.checkFileExists(this.cacheDataFileName)
119
+ if (!cacheFileExists) {
120
+ return
121
+ }
122
+
123
+ const data = await this.loadFileFromLocalStorage(this.cacheDataFileName)
124
+ if (!data) {
125
+ return
126
+ }
127
+
128
+ const cacheData: CacheDataFile = JSON.parse(data)
129
+
130
+ return cacheData
131
+ }
132
+
133
+ /**
134
+ * Saves the cache data to local storage.
135
+ *
136
+ * @param cacheData - The cache data to be saved.
137
+ * @returns A promise that resolves to a boolean indicating whether the save operation was successful.
138
+ */
139
+ private saveCacheData = async (cacheData: CacheDataFile): Promise<boolean> => {
140
+ const cacheDataAsString = JSON.stringify(cacheData)
141
+
142
+ return this.saveFileToLocalStorage(this.cacheDataFileName, cacheDataAsString)
143
+ }
144
+
145
+ /**
146
+ * Processes the queue of OCABundleQueueEntry items.
147
+ *
148
+ * @param items - An array of OCABundleQueueEntry items to process.
149
+ * @returns A promise that resolves to an array of processed OCABundleQueueEntry items.
150
+ */
151
+ private processQueue = async (items: Array<OCABundleQueueEntry>): Promise<OCABundleQueueEntry[]> => {
152
+ const processed = Array<OCABundleQueueEntry>()
153
+ const operations = []
154
+
155
+ for (const q of items) {
156
+ const hash = q.sha256
157
+
158
+ this.log?.info(`Processing op ${q.operation} for ${hash}`)
159
+
160
+ switch (q.operation) {
161
+ case OCABundleQueueEntryOperation.Add:
162
+ {
163
+ const path = this.findPathBySha256(hash)
164
+ if (!path) {
165
+ continue
166
+ }
167
+ operations.push(this.fetchOCABundle(path))
168
+ }
169
+ break
170
+ case OCABundleQueueEntryOperation.Remove:
171
+ operations.push(this.removeFileFromLocalStorage(hash))
172
+ break
173
+ }
174
+ }
175
+
176
+ try {
177
+ // Check which operations were successful, and remove them from
178
+ // the queue.
179
+ const settled = await Promise.allSettled(operations)
180
+ for (const i in settled) {
181
+ if (settled[i].status === 'fulfilled') {
182
+ processed.push(items[i])
183
+ }
184
+ }
185
+
186
+ return Array.from(processed) ?? []
187
+ } catch (error) {
188
+ this.log?.error(`Failed to process some operations, ${error}`)
189
+
190
+ return []
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Prepares the bundle queue based on the new and old index files.
196
+ * It compares the SHA256 hashes of the files in the new and old index files
197
+ * and determines which files should be removed and which files should be added
198
+ * to the bundle queue.
199
+ *
200
+ * @param newIndexFile - The new bundle index file.
201
+ * @param oldIndexFile - The old bundle index file.
202
+ * @returns An array of `OCABundleQueueEntry` objects representing the files to be removed and added.
203
+ */
204
+ private prepareBundleQueue = (newIndexFile: BundleIndex, oldIndexFile: BundleIndex): Array<OCABundleQueueEntry> => {
205
+ const oldIndexItemHashes = [...new Set(Object.keys(oldIndexFile).map((key) => oldIndexFile[key].sha256))]
206
+ const newIndexItemHashes = [...new Set(Object.keys(newIndexFile).map((key) => newIndexFile[key].sha256))]
207
+
208
+ // if the SHA256 is in the old index file but not in
209
+ // the new index file, it should be removed.
210
+ const hashesToRemove = oldIndexItemHashes
211
+ .filter((hash) => !newIndexItemHashes.includes(hash))
212
+ .map((hash) => ({
213
+ sha256: hash,
214
+ operation: OCABundleQueueEntryOperation.Remove,
215
+ }))
216
+
217
+ // if the SHA256 is in the new index file but not in
218
+ // the old index file, it should be added.
219
+ const hashesToAdd = newIndexItemHashes
220
+ .filter((hash) => !oldIndexItemHashes.includes(hash))
221
+ .map((hash) => ({
222
+ sha256: hash,
223
+ operation: OCABundleQueueEntryOperation.Add,
224
+ }))
225
+
226
+ this.log?.info(`Files to remove ${hashesToRemove.length}, add ${hashesToAdd.length}`)
227
+
228
+ return [...hashesToRemove, ...hashesToAdd]
229
+ }
230
+
231
+ /**
232
+ * Finds the SHA-256 hash associated with a given path in the index file.
233
+ *
234
+ * @param {string} path - The path to search for in the index file.
235
+ * @returns {string | null} The SHA-256 hash associated with the path if found, or null if not found.
236
+ */
237
+ private findSha256ByPath = (path: string): string | null => {
238
+ for (const key in this.indexFile) {
239
+ if (this.indexFile[key].path === path) {
240
+ return this.indexFile[key].sha256
241
+ }
242
+ }
243
+
244
+ return null
245
+ }
246
+
247
+ /**
248
+ * Finds the path associated with the given SHA256 hash in the index file.
249
+ *
250
+ * @param sha256 - The SHA256 hash to search for.
251
+ * @returns The path associated with the SHA256 hash, or null if not found.
252
+ */
253
+ private findPathBySha256 = (sha256: string): string | null => {
254
+ for (const key of Object.keys(this.indexFile)) {
255
+ const hash = this.indexFile[key].sha256
256
+ if (hash === sha256) {
257
+ return this.indexFile[key].path
258
+ }
259
+ }
260
+
261
+ return null
262
+ }
263
+
264
+ /**
265
+ * Returns the file name for the bundle at the specified path.
266
+ *
267
+ * @param path - The path of the bundle.
268
+ * @returns The file name for the bundle, or null if not found.
269
+ */
270
+ private fileNameForBundleAtPath = (path: string): string | null => {
271
+ return this.findSha256ByPath(path)
272
+ }
273
+
274
+ /**
275
+ * Returns the file storage path for the remote OCA resolver.
276
+ * The file storage path is the concatenation of the `CachesDirectoryPath` and `ocaBundleStorageDirectory`.
277
+ *
278
+ * @returns The file storage path.
279
+ */
280
+ private fileStoragePath = (): string => {
281
+ return `${CachesDirectoryPath}/${ocaBundleStorageDirectory}`
282
+ }
283
+
284
+ /**
285
+ * Checks if a file exists at a specific path in the document directory.
286
+ *
287
+ * @param {string} fileName - The name of the file to check.
288
+ * @returns {Promise<boolean>} A promise that resolves to true if the file exists, or false otherwise.
289
+ * @throws Will throw an error if the existence check fails.
290
+ */
291
+ private checkFileExists = async (fileName: string): Promise<boolean> => {
292
+ const pathToFile = `${this.fileStoragePath()}/${fileName}`
293
+
294
+ try {
295
+ const fileExists = await exists(pathToFile)
296
+ this.log?.info(`File ${fileName} ${fileExists ? 'does' : 'does not'} exist at ${pathToFile}`)
297
+
298
+ return fileExists
299
+ } catch (error) {
300
+ this.log?.error(`Failed to check existence of ${fileName} at ${pathToFile}`)
301
+ }
302
+
303
+ return false
304
+ }
305
+
306
+ /**
307
+ * Creates a working directory if it does not already exist.
308
+ *
309
+ * @returns A promise that resolves to a boolean indicating whether the directory was created successfully.
310
+ */
311
+ private createWorkingDirectoryIfNotExists = async (): Promise<boolean> => {
312
+ const workSpace = this.fileStoragePath()
313
+ const pathDoesExist = await exists(workSpace)
314
+
315
+ if (!pathDoesExist) {
316
+ try {
317
+ await mkdir(workSpace)
318
+ return true
319
+ } catch (error) {
320
+ this.log?.error(`Failed to create directory ${workSpace}`)
321
+ return false
322
+ }
323
+ }
324
+
325
+ return true
326
+ }
327
+
328
+ /**
329
+ * Saves a string of data to a file in the local storage.
330
+ *
331
+ * @param {string} fileName - The name of the file to save.
332
+ * @param {string} data - The data to write to the file.
333
+ * @returns {Promise<boolean>} A promise that resolves to true if the file was saved successfully, or false otherwise.
334
+ * @throws Will throw an error if the write operation fails.
335
+ */
336
+ private saveFileToLocalStorage = async (fileName: string, data: string): Promise<boolean> => {
337
+ const pathToFile = `${this.fileStoragePath()}/${fileName}`
338
+
339
+ try {
340
+ await writeFile(pathToFile, data, 'utf8')
341
+ this.log?.info(`File ${fileName} saved to ${pathToFile}`)
342
+
343
+ return true
344
+ } catch (error) {
345
+ this.log?.error(`Failed to save file ${fileName} to ${pathToFile}, ${error}`)
346
+ }
347
+
348
+ return false
349
+ }
350
+
351
+ /**
352
+ * Loads a file from local storage.
353
+ *
354
+ * @param fileName - The name of the file to load.
355
+ * @returns A promise that resolves to the contents of the file, or undefined if the file does not exist.
356
+ */
357
+ private loadFileFromLocalStorage = async (fileName: string): Promise<string | undefined> => {
358
+ const pathToFile = `${this.fileStoragePath()}/${fileName}`
359
+
360
+ try {
361
+ const fileExists = await this.checkFileExists(fileName)
362
+ if (!fileExists) {
363
+ this.log?.warn(`Missing ${fileName} from ${pathToFile}`)
364
+
365
+ return
366
+ }
367
+
368
+ const data = await readFile(pathToFile, 'utf8')
369
+ this.log?.info(`File ${fileName} loaded from ${pathToFile}`)
370
+
371
+ return data
372
+ } catch (error) {
373
+ this.log?.error(`Failed to load file ${fileName} from ${pathToFile}`)
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Removes a file from the local storage.
379
+ *
380
+ * @param fileName - The name of the file to be removed.
381
+ * @returns A promise that resolves to a boolean indicating whether the file was successfully removed.
382
+ */
383
+ private removeFileFromLocalStorage = async (fileName: string): Promise<boolean> => {
384
+ const pathToFile = `${this.fileStoragePath()}/${fileName}`
385
+
386
+ try {
387
+ await unlink(pathToFile)
388
+ return true
389
+ } catch (error) {
390
+ this.log?.error(`Failed to unlink file ${fileName} from ${pathToFile}`)
391
+ }
392
+
393
+ return false
394
+ }
395
+
396
+ /**
397
+ * Fetches an OCA bundle from a remote resource and saves it to local storage.
398
+ * @param path - The path of the remote resource.
399
+ * @returns A promise that resolves to a boolean indicating whether the fetch and save operation was successful.
400
+ */
401
+ private fetchOCABundle = async (path: string): Promise<boolean> => {
402
+ const response = await this.axiosInstance.get(path)
403
+ const { status } = response
404
+
405
+ if (status !== 200) {
406
+ this.log?.error(`Failed to fetch remote resource at ${path}`)
407
+
408
+ return false
409
+ }
410
+
411
+ const fileName = this.fileNameForBundleAtPath(path)
412
+ if (!fileName) {
413
+ this.log?.error(`Failed to determine file name ${fileName} for save`)
414
+
415
+ return false
416
+ }
417
+
418
+ return this.saveFileToLocalStorage(fileName, JSON.stringify(response.data))
419
+ }
420
+
421
+ /**
422
+ * Loads the OCA index from a remote location and processes it.
423
+ * If the remote resource is not available, it falls back to the cached index file.
424
+ * If the index file has not changed, it uses the existing data.
425
+ * If the index file has changed, it refreshes the index file and the bundles.
426
+ *
427
+ * @param filePath - The path to the remote OCA index file.
428
+ * @returns A Promise that resolves when the index file and bundles have been processed.
429
+ * @throws If there is an error fetching or processing the OCA index.
430
+ */
431
+ private loadOCAIndex = async (filePath: string) => {
432
+ try {
433
+ const response = await this.axiosInstance.get(filePath)
434
+ const { status } = response
435
+ const { etag } = response.headers
436
+
437
+ if (status !== 200) {
438
+ this.log?.error(`Failed to fetch remote resource at ${filePath}`)
439
+ // failed to fetch, use the cached index file
440
+ // if available
441
+ const data = await this.loadFileFromLocalStorage(filePath)
442
+ if (data) {
443
+ this.log?.info(`Using index file ${filePath} from cache`)
444
+ this.indexFile = JSON.parse(data)
445
+ }
446
+
447
+ return
448
+ }
449
+
450
+ if (etag && etag === this.indexFileEtag) {
451
+ this.log?.info(`Index file ${filePath} has not changed, etag ${etag}`)
452
+ // etag is the same, no need to refresh
453
+ this.indexFile = response.data
454
+
455
+ const fetchMissingFiles = true
456
+ const status = await this.verifyCacheIntegrity(fetchMissingFiles)
457
+ if (!status) {
458
+ this.log?.error(`Cache integrity broken, unable to re-fetch missing files`)
459
+ }
460
+
461
+ return
462
+ }
463
+
464
+ // etag is different, we need to refresh the
465
+ // index file and the bundles.
466
+ const items = this.prepareBundleQueue(response.data, this.indexFile)
467
+ this.indexFile = response.data
468
+ this.indexFileEtag = etag
469
+
470
+ const processed = await this.processQueue(items)
471
+ if (processed.length !== items.length) {
472
+ this.log?.info(`Processed ${processed.length} items, expected ${items.length}`)
473
+ }
474
+
475
+ await this.saveFileToLocalStorage(filePath, JSON.stringify(this.indexFile))
476
+ } catch (error) {
477
+ this.log?.error(`Failed to fetch OCA index ${filePath}`)
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Loads the OCABundle from the specified path.
483
+ *
484
+ * @param path - The path of the OCABundle.
485
+ * @returns A promise that resolves to an array of IOverlayBundleData.
486
+ */
487
+ private loadOCABundle = async (path: string): Promise<IOverlayBundleData[]> => {
488
+ // check if the file exists in the local storage
489
+ // if it does, load it from there.
490
+ const fileName = this.fileNameForBundleAtPath(path)
491
+ if (!fileName) {
492
+ this.log?.error(`Failed to determine file name ${fileName} for save`)
493
+ return []
494
+ }
495
+
496
+ const cachedData = await this.loadFileFromLocalStorage(fileName)
497
+ if (cachedData) {
498
+ return JSON.parse(cachedData)
499
+ }
500
+
501
+ return []
502
+ }
503
+
504
+ /**
505
+ * Finds a matching identifier in the index file based on the provided identifiers.
506
+ * The order of the identifiers matters if more than one candidate exists in the index file.
507
+ *
508
+ * @param identifiers - The identifiers to match against the index file.
509
+ * @returns The matching identifier, or undefined if no match is found.
510
+ */
511
+ private matchBundleIndexEntry = (identifiers: Identifiers): string | undefined => {
512
+ const { schemaId, credentialDefinitionId, templateId } = identifiers
513
+ // also check unqualified schema and cred def id's if qualified versions exist
514
+ const unqualifiedSchemaId = schemaId?.startsWith('did:indy:') ? getUnQualifiedDidIndyDid(schemaId) : undefined
515
+ const unqualifiedCredDefId = credentialDefinitionId?.startsWith('did:indy:')
516
+ ? getUnQualifiedDidIndyDid(credentialDefinitionId)
517
+ : undefined
518
+
519
+ // If more than one candidate identifier exists in the index file then
520
+ // order matters here.
521
+ const candidates = [schemaId, unqualifiedSchemaId, credentialDefinitionId, unqualifiedCredDefId, templateId].filter(
522
+ (value) => value !== undefined && value !== null && value !== ''
523
+ )
524
+
525
+ if (candidates.length === 0) {
526
+ return undefined
527
+ }
528
+
529
+ const keys = Object.keys(this.indexFile)
530
+ const identifier = candidates.find((c) => keys.includes(c!))
531
+
532
+ return identifier
533
+ }
534
+
535
+ /**
536
+ * Resolves the OCABundle based on the given parameters.
537
+ * @param params - The parameters for resolving the OCABundle.
538
+ * @param params.identifiers - The identifiers used to match the OCABundle.
539
+ * @param params.language - The language of the OCABundle (optional).
540
+ * @returns A Promise that resolves to the OCABundle or undefined.
541
+ */
542
+ public async resolve(params: {
543
+ identifiers: Identifiers
544
+ language?: string | undefined
545
+ }): Promise<OCABundle | undefined> {
546
+ const language = params.language || defaultBundleLanguage
547
+ const identifier = this.matchBundleIndexEntry(params.identifiers)
548
+
549
+ if (!identifier || !(identifier in this.bundles || identifier in this.indexFile)) {
550
+ return Promise.resolve(undefined)
551
+ }
552
+
553
+ if (identifier in this.bundles) {
554
+ return Promise.resolve(
555
+ new OCABundle(this.bundles[identifier] as OverlayBundle, {
556
+ ...this.options,
557
+ language: language ?? this.options.language,
558
+ })
559
+ )
560
+ }
561
+
562
+ const bundlePath = this.indexFile[identifier]?.path
563
+
564
+ if (!bundlePath) {
565
+ return Promise.resolve(undefined)
566
+ }
567
+
568
+ try {
569
+ const bundleData: IOverlayBundleData[] = await this.loadOCABundle(bundlePath)
570
+ const overlayBundle = new OverlayBundle(params.identifiers.credentialDefinitionId ?? '', bundleData[0])
571
+ const overlay = overlayBundle.overlays.find(
572
+ (overlay) => overlay.type === BrandingOverlayType.Branding01 || overlay.type === BrandingOverlayType.Branding10
573
+ )
574
+
575
+ if (!overlay) {
576
+ overlayBundle.overlays.push(
577
+ ...this.getFallbackBrandingOverlays(overlayBundle.credentialDefinitionId, overlayBundle.captureBase.digest)
578
+ )
579
+ }
580
+
581
+ this.bundles[identifier] = overlayBundle
582
+
583
+ return new OCABundle(overlayBundle, {
584
+ ...this.options,
585
+ language: language ?? this.options.language,
586
+ })
587
+ } catch (error) {
588
+ // probably couldn't parse the overlay bundle.
589
+ return Promise.resolve(undefined)
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Retrieves the fallback branding overlays for a given credential definition ID and capture base.
595
+ * @param credentialDefinitionId - The ID of the credential definition.
596
+ * @param captureBase - The capture base for the overlays.
597
+ * @returns An array of fallback branding overlays.
598
+ */
599
+ private getFallbackBrandingOverlays(credentialDefinitionId: string, captureBase: string): BaseOverlay[] {
600
+ const legacyBrandingOverlay: LegacyBrandingOverlay = new LegacyBrandingOverlay(credentialDefinitionId, {
601
+ capture_base: captureBase,
602
+ type: BrandingOverlayType.Branding01,
603
+ background_color: generateColor(credentialDefinitionId),
604
+ })
605
+
606
+ const brandingOverlay: BrandingOverlay = new BrandingOverlay(credentialDefinitionId, {
607
+ capture_base: captureBase,
608
+ type: BrandingOverlayType.Branding10,
609
+ primary_background_color: generateColor(credentialDefinitionId),
610
+ })
611
+
612
+ return [legacyBrandingOverlay, brandingOverlay]
613
+ }
614
+
615
+ private async verifyCacheIntegrity(fetchMissing: boolean = false): Promise<boolean> {
616
+ const indexItemHashes = [...new Set(Object.keys(this.indexFile).map((key) => this.indexFile[key].sha256))]
617
+ const queue = []
618
+
619
+ this.log?.info(`Checking bundle cache integrity`)
620
+
621
+ for (const hash of indexItemHashes) {
622
+ const fileName = this.fileNameForBundleAtPath(this.findPathBySha256(hash)!)
623
+ if (!fileName) {
624
+ continue
625
+ }
626
+
627
+ const fileExists = await this.checkFileExists(fileName)
628
+ if (!fileExists) {
629
+ this.log?.warn(`File ${fileName} does not exist, re-fetching`)
630
+ queue.push({
631
+ sha256: hash,
632
+ operation: OCABundleQueueEntryOperation.Add,
633
+ })
634
+ }
635
+ }
636
+
637
+ if (queue.length === 0) {
638
+ this.log?.info(`Cache integrity verified`)
639
+
640
+ return true
641
+ }
642
+
643
+ if (queue.length > 0 && !fetchMissing) {
644
+ this.log?.info(`Missing ${queue.length} files, cache broken`)
645
+
646
+ return false
647
+ }
648
+
649
+ const processed = await this.processQueue(queue)
650
+
651
+ if (processed.length !== queue.length) {
652
+ this.log?.error(`Processed ${processed.length} items, expected ${queue.length}`)
653
+
654
+ return false
655
+ }
656
+
657
+ this.log?.info(`Cache was broken, now fixed`)
658
+
659
+ return true
660
+ }
661
+ }
@@ -0,0 +1,25 @@
1
+ import BaseOverlay from './base/BaseOverlay'
2
+ import BrandingOverlay from './branding/BrandingOverlay'
3
+ import LegacyBrandingOverlay from './branding/LegacyBrandingOverlay'
4
+ import CharacterEncodingOverlay from './semantic/CharacterEncodingOverlay'
5
+ import FormatOverlay from './semantic/FormatOverlay'
6
+ import InformationOverlay from './semantic/InformationOverlay'
7
+ import LabelOverlay from './semantic/LabelOverlay'
8
+ import MetaOverlay from './semantic/MetaOverlay'
9
+ import StandardOverlay from './semantic/StandardOverlay'
10
+
11
+ const OverlayTypeMap: Map<string, typeof BaseOverlay | typeof BrandingOverlay | typeof LegacyBrandingOverlay> = new Map(
12
+ Object.entries({
13
+ 'spec/overlays/character_encoding/1.0': CharacterEncodingOverlay,
14
+ 'spec/overlays/label/1.0': LabelOverlay,
15
+ 'spec/overlays/information/1.0': InformationOverlay,
16
+ 'spec/overlays/format/1.0': FormatOverlay,
17
+ 'spec/overlays/standard/1.0': StandardOverlay,
18
+ 'spec/overlays/meta/1.0': MetaOverlay,
19
+ 'aries/overlays/branding/1.0': BrandingOverlay,
20
+ 'aries/overlays/branding/1.1': BrandingOverlay,
21
+ 'aries/overlays/branding/0.1': LegacyBrandingOverlay,
22
+ })
23
+ )
24
+
25
+ export default OverlayTypeMap
@@ -0,0 +1,20 @@
1
+ export enum CaptureBaseAttributeType {
2
+ Binary = 'Binary',
3
+ Text = 'Text',
4
+ DateTime = 'DateTime',
5
+ Numeric = 'Numeric',
6
+ DateInt = 'DateInt',
7
+ }
8
+
9
+ export enum OverlayType {
10
+ CaptureBase10 = 'spec/capture_base/1.0',
11
+ Meta10 = 'spec/overlays/meta/1.0',
12
+ Label10 = 'spec/overlays/label/1.0',
13
+ Format10 = 'spec/overlays/format/1.0',
14
+ CharacterEncoding10 = 'spec/overlays/character_encoding/1.0',
15
+ Standard10 = 'spec/overlays/standard/1.0',
16
+ Information10 = 'spec/overlays/information/1.0',
17
+ Branding01 = 'aries/overlays/branding/0.1',
18
+ Branding10 = 'aries/overlays/branding/1.0',
19
+ Branding11 = 'aries/overlays/branding/1.1',
20
+ }