@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.
- package/README.md +33 -0
- package/build/__tests__/remote.test.d.ts +1 -0
- package/build/__tests__/remote.test.js +162 -0
- package/build/constants.d.ts +4 -0
- package/build/constants.js +7 -0
- package/build/formatters/credential/CredentialFormatter.d.ts +8 -0
- package/build/formatters/credential/CredentialFormatter.js +32 -0
- package/build/formatters/credential/DisplayAttribute.d.ts +11 -0
- package/build/formatters/credential/DisplayAttribute.js +18 -0
- package/build/formatters/credential/LocalizedCredential.d.ts +13 -0
- package/build/formatters/credential/LocalizedCredential.js +57 -0
- package/build/formatters/credential/index.d.ts +4 -0
- package/build/formatters/credential/index.js +12 -0
- package/build/formatters/index.d.ts +4 -0
- package/build/formatters/index.js +12 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +34 -0
- package/build/interfaces/data/base/BaseOverlayData.interface.d.ts +5 -0
- package/build/interfaces/data/base/BaseOverlayData.interface.js +2 -0
- package/build/interfaces/data/branding/BrandingOverlayData.interface.d.ts +12 -0
- package/build/interfaces/data/branding/BrandingOverlayData.interface.js +2 -0
- package/build/interfaces/data/branding/LegacyBrandingOverlayData.interface.d.ts +15 -0
- package/build/interfaces/data/branding/LegacyBrandingOverlayData.interface.js +2 -0
- package/build/interfaces/data/bundle/OverlayBundleData.interface.d.ts +6 -0
- package/build/interfaces/data/bundle/OverlayBundleData.interface.js +2 -0
- package/build/interfaces/data/capture-base/CaptureBaseData.interface.d.ts +7 -0
- package/build/interfaces/data/capture-base/CaptureBaseData.interface.js +2 -0
- package/build/interfaces/data/index.d.ts +12 -0
- package/build/interfaces/data/index.js +2 -0
- package/build/interfaces/data/semantic/CharacterEncodingOverlayData.interface.d.ts +6 -0
- package/build/interfaces/data/semantic/CharacterEncodingOverlayData.interface.js +2 -0
- package/build/interfaces/data/semantic/FormatOverlayData.interface.d.ts +4 -0
- package/build/interfaces/data/semantic/FormatOverlayData.interface.js +2 -0
- package/build/interfaces/data/semantic/InformationOverlayData.interface.d.ts +5 -0
- package/build/interfaces/data/semantic/InformationOverlayData.interface.js +2 -0
- package/build/interfaces/data/semantic/LabelOverlayData.interface.d.ts +7 -0
- package/build/interfaces/data/semantic/LabelOverlayData.interface.js +2 -0
- package/build/interfaces/data/semantic/MetaOverlayData.interface.d.ts +12 -0
- package/build/interfaces/data/semantic/MetaOverlayData.interface.js +2 -0
- package/build/interfaces/data/semantic/StandardOverlayData.interface.d.ts +5 -0
- package/build/interfaces/data/semantic/StandardOverlayData.interface.js +2 -0
- package/build/interfaces/index.d.ts +2 -0
- package/build/interfaces/index.js +18 -0
- package/build/interfaces/overlay/OverlayBundleAttribute.interface.d.ts +9 -0
- package/build/interfaces/overlay/OverlayBundleAttribute.interface.js +2 -0
- package/build/interfaces/overlay/OverlayBundleMetadata.interface.d.ts +9 -0
- package/build/interfaces/overlay/OverlayBundleMetadata.interface.js +2 -0
- package/build/interfaces/overlay/index.d.ts +3 -0
- package/build/interfaces/overlay/index.js +2 -0
- package/build/legacy/index.d.ts +3 -0
- package/build/legacy/index.js +19 -0
- package/build/legacy/resolver/oca.d.ts +99 -0
- package/build/legacy/resolver/oca.js +239 -0
- package/build/legacy/resolver/record.d.ts +50 -0
- package/build/legacy/resolver/record.js +37 -0
- package/build/legacy/resolver/remote-oca.d.ts +186 -0
- package/build/legacy/resolver/remote-oca.js +536 -0
- package/build/types/OverlayTypeMap.d.ts +5 -0
- package/build/types/OverlayTypeMap.js +25 -0
- package/build/types/TypeEnums.d.ts +19 -0
- package/build/types/TypeEnums.js +24 -0
- package/build/types/base/BaseOverlay.d.ts +8 -0
- package/build/types/base/BaseOverlay.js +28 -0
- package/build/types/branding/BrandingOverlay.d.ts +15 -0
- package/build/types/branding/BrandingOverlay.js +68 -0
- package/build/types/branding/LegacyBrandingOverlay.d.ts +18 -0
- package/build/types/branding/LegacyBrandingOverlay.js +51 -0
- package/build/types/bundle/OverlayBundle.d.ts +20 -0
- package/build/types/bundle/OverlayBundle.js +167 -0
- package/build/types/capture-base/CaptureBase.d.ts +10 -0
- package/build/types/capture-base/CaptureBase.js +30 -0
- package/build/types/index.d.ts +14 -0
- package/build/types/index.js +33 -0
- package/build/types/semantic/CharacterEncodingOverlay.d.ts +9 -0
- package/build/types/semantic/CharacterEncodingOverlay.js +43 -0
- package/build/types/semantic/FormatOverlay.d.ts +7 -0
- package/build/types/semantic/FormatOverlay.js +30 -0
- package/build/types/semantic/InformationOverlay.d.ts +8 -0
- package/build/types/semantic/InformationOverlay.js +31 -0
- package/build/types/semantic/LabelOverlay.d.ts +10 -0
- package/build/types/semantic/LabelOverlay.js +41 -0
- package/build/types/semantic/MetaOverlay.d.ts +15 -0
- package/build/types/semantic/MetaOverlay.js +54 -0
- package/build/types/semantic/StandardOverlay.d.ts +8 -0
- package/build/types/semantic/StandardOverlay.js +38 -0
- package/build/utils/color/generateColor.d.ts +2 -0
- package/build/utils/color/generateColor.js +11 -0
- package/build/utils/color/hashCode.d.ts +8 -0
- package/build/utils/color/hashCode.js +12 -0
- package/build/utils/color/hashToRGBA.d.ts +8 -0
- package/build/utils/color/hashToRGBA.js +23 -0
- package/build/utils/color/index.d.ts +5 -0
- package/build/utils/color/index.js +15 -0
- package/build/utils/color/luminanceForHexColor.d.ts +7 -0
- package/build/utils/color/luminanceForHexColor.js +18 -0
- package/build/utils/color/mulberry32.d.ts +9 -0
- package/build/utils/color/mulberry32.js +16 -0
- package/build/utils/color/textColorForBackground.d.ts +1 -0
- package/build/utils/color/textColorForBackground.js +24 -0
- package/build/utils/credential-definition.d.ts +1 -0
- package/build/utils/credential-definition.js +21 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.js +17 -0
- package/build/utils/logger.d.ts +5 -0
- package/build/utils/logger.js +17 -0
- package/build/utils/schema.d.ts +4 -0
- package/build/utils/schema.js +19 -0
- package/package.json +53 -0
- package/src/__tests__/__snapshots__/remote.test.ts.snap +293 -0
- package/src/__tests__/fixtures/bundle.json +131 -0
- package/src/__tests__/fixtures/oca.json +4 -0
- package/src/__tests__/fixtures/ocabundles.json +142 -0
- package/src/__tests__/remote.test.ts +180 -0
- package/src/constants.ts +7 -0
- package/src/formatters/credential/CredentialFormatter.ts +20 -0
- package/src/formatters/credential/DisplayAttribute.ts +29 -0
- package/src/formatters/credential/LocalizedCredential.ts +53 -0
- package/src/formatters/credential/index.ts +5 -0
- package/src/formatters/index.ts +5 -0
- package/src/index.ts +5 -0
- package/src/interfaces/data/base/BaseOverlayData.interface.ts +5 -0
- package/src/interfaces/data/branding/BrandingOverlayData.interface.ts +13 -0
- package/src/interfaces/data/branding/LegacyBrandingOverlayData.interface.ts +16 -0
- package/src/interfaces/data/bundle/OverlayBundleData.interface.ts +7 -0
- package/src/interfaces/data/capture-base/CaptureBaseData.interface.ts +7 -0
- package/src/interfaces/data/index.ts +25 -0
- package/src/interfaces/data/semantic/CharacterEncodingOverlayData.interface.ts +8 -0
- package/src/interfaces/data/semantic/FormatOverlayData.interface.ts +5 -0
- package/src/interfaces/data/semantic/InformationOverlayData.interface.ts +6 -0
- package/src/interfaces/data/semantic/LabelOverlayData.interface.ts +8 -0
- package/src/interfaces/data/semantic/MetaOverlayData.interface.ts +13 -0
- package/src/interfaces/data/semantic/StandardOverlayData.interface.ts +7 -0
- package/src/interfaces/index.ts +2 -0
- package/src/interfaces/overlay/OverlayBundleAttribute.interface.ts +9 -0
- package/src/interfaces/overlay/OverlayBundleMetadata.interface.ts +9 -0
- package/src/interfaces/overlay/index.ts +4 -0
- package/src/legacy/index.ts +3 -0
- package/src/legacy/resolver/oca.ts +377 -0
- package/src/legacy/resolver/record.ts +81 -0
- package/src/legacy/resolver/remote-oca.ts +661 -0
- package/src/types/OverlayTypeMap.ts +25 -0
- package/src/types/TypeEnums.ts +20 -0
- package/src/types/base/BaseOverlay.ts +18 -0
- package/src/types/branding/BrandingOverlay.ts +61 -0
- package/src/types/branding/LegacyBrandingOverlay.ts +47 -0
- package/src/types/bundle/OverlayBundle.ts +203 -0
- package/src/types/capture-base/CaptureBase.ts +22 -0
- package/src/types/index.ts +30 -0
- package/src/types/semantic/CharacterEncodingOverlay.ts +30 -0
- package/src/types/semantic/FormatOverlay.ts +15 -0
- package/src/types/semantic/InformationOverlay.ts +18 -0
- package/src/types/semantic/LabelOverlay.ts +30 -0
- package/src/types/semantic/MetaOverlay.ts +48 -0
- package/src/types/semantic/StandardOverlay.ts +24 -0
- package/src/utils/color/generateColor.ts +8 -0
- package/src/utils/color/hashCode.ts +11 -0
- package/src/utils/color/hashToRGBA.ts +22 -0
- package/src/utils/color/index.ts +7 -0
- package/src/utils/color/luminanceForHexColor.ts +19 -0
- package/src/utils/color/mulberry32.ts +15 -0
- package/src/utils/color/textColorForBackground.ts +18 -0
- package/src/utils/credential-definition.ts +19 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/logger.ts +14 -0
- 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
|
+
}
|