@cooperation/vc-storage 1.0.21 → 1.0.23

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.
@@ -5,11 +5,9 @@ import { v4 as uuidv4 } from 'uuid';
5
5
  import { extractKeyPairFromCredential, generateDIDSchema, generateUnsignedRecommendation, generateUnsignedVC, } from '../utils/credential.js';
6
6
  import { customDocumentLoader } from '../utils/digitalbazaar.js';
7
7
  import { saveToGoogleDrive } from '../utils/google.js';
8
- import { GoogleDriveStorage } from './GoogleDriveStorage.js';
9
8
  /**
10
9
  * Class representing the Credential Engine.
11
10
  * @class CredentialEngine
12
- * @param {string} accessToken - The access token for the user.
13
11
  * @classdesc Credential Engine class to create DIDs and VCs.
14
12
  * @method createDID - Create a new DID with Digital Bazaar's Ed25519VerificationKey2020 key pair.
15
13
  * @method createWalletDID - Create a new DID with user metamask address as controller.
@@ -21,8 +19,8 @@ import { GoogleDriveStorage } from './GoogleDriveStorage.js';
21
19
  export class CredentialEngine {
22
20
  storage;
23
21
  keyPair;
24
- constructor(accessToken) {
25
- this.storage = new GoogleDriveStorage(accessToken);
22
+ constructor(storage) {
23
+ this.storage = storage;
26
24
  }
27
25
  async getKeyPair(vc) {
28
26
  // Fetch all stored key pairs
@@ -73,11 +71,11 @@ export class CredentialEngine {
73
71
  async createDID() {
74
72
  try {
75
73
  const keyPair = await this.generateKeyPair();
76
- const keyFile = await saveToGoogleDrive({
77
- storage: this.storage,
78
- data: keyPair,
79
- type: 'KEYPAIR',
80
- });
74
+ // const keyFile = await saveToGoogleDrive({
75
+ // storage: this.storage,
76
+ // data: keyPair,
77
+ // type: 'KEYPAIR',
78
+ // });
81
79
  const didDocument = await generateDIDSchema(keyPair);
82
80
  return { didDocument, keyPair };
83
81
  }
@@ -132,19 +130,14 @@ export class CredentialEngine {
132
130
  * @throws Will throw an error if VC signing fails.
133
131
  */
134
132
  async signVC({ data, type, keyPair, issuerId, vcFileId }) {
135
- console.log('🚀 ~ CredentialEngine ~ signVC ~ { data, type, keyPair, issuerId, vcFileId }:', {
136
- data,
137
- type,
138
- keyPair,
139
- issuerId,
140
- vcFileId,
141
- });
142
- let vc;
143
133
  let credential = generateUnsignedVC({ formData: data, issuerDid: issuerId });
144
134
  if (type == 'RECOMMENDATION' && vcFileId) {
145
135
  console.log('WOW');
146
- vc = (await this.storage.retrieve(vcFileId));
147
- credential = generateUnsignedRecommendation({ vc, recommendation: data, issuerDid: issuerId });
136
+ credential = generateUnsignedRecommendation({
137
+ vcId: vcFileId,
138
+ recommendation: data,
139
+ issuerDid: issuerId,
140
+ });
148
141
  }
149
142
  try {
150
143
  console.log('🚀 ~ CredentialEngine ~ signVC ~ credential:', credential);
@@ -15,6 +15,58 @@
15
15
  */
16
16
  export class GoogleDriveStorage {
17
17
  accessToken;
18
+ folderCache = {};
19
+ fileIdsCache = null;
20
+ async updateFileIdsJson(newFileId) {
21
+ const constructUrl = () => {
22
+ const baseUrl = 'https://www.googleapis.com/drive/v3/files';
23
+ const queryParams = new URLSearchParams({
24
+ spaces: 'appDataFolder',
25
+ q: "name='file_ids.json'",
26
+ fields: 'files(id)',
27
+ });
28
+ return `${baseUrl}?${queryParams.toString()}`;
29
+ };
30
+ try {
31
+ // ✅ Fetch `file_ids.json` ID once per session (cached)
32
+ if (!this.fileIdsCache) {
33
+ const existingFile = await this.fetcher({
34
+ method: 'GET',
35
+ headers: {},
36
+ url: constructUrl(),
37
+ });
38
+ if (existingFile.files.length > 0) {
39
+ this.fileIdsCache = existingFile.files[0].id;
40
+ }
41
+ else {
42
+ console.log('No existing file_ids.json found, creating a new one.');
43
+ this.fileIdsCache = null;
44
+ }
45
+ }
46
+ let existingFileIds = [];
47
+ // ✅ Fetch existing file IDs **only if `file_ids.json` exists**
48
+ if (this.fileIdsCache) {
49
+ try {
50
+ const fileContent = await this.fetcher({
51
+ method: 'GET',
52
+ headers: {},
53
+ url: `https://www.googleapis.com/drive/v3/files/${this.fileIdsCache}?alt=media`,
54
+ });
55
+ existingFileIds = fileContent;
56
+ }
57
+ catch (error) {
58
+ console.log('Error fetching existing file_ids.json content, creating new list.');
59
+ }
60
+ }
61
+ // ✅ Append the new file ID to the list
62
+ existingFileIds.push(newFileId);
63
+ console.log('File ID saved to appDataFolder.', this.fileIdsCache);
64
+ }
65
+ catch (error) {
66
+ console.error('Error updating file_ids.json:', error.message);
67
+ throw error;
68
+ }
69
+ }
18
70
  constructor(accessToken) {
19
71
  this.accessToken = accessToken;
20
72
  }
@@ -28,7 +80,6 @@ export class GoogleDriveStorage {
28
80
  },
29
81
  body,
30
82
  });
31
- // Check the Content-Type to ensure it's JSON before parsing
32
83
  const contentType = res.headers.get('Content-Type') || '';
33
84
  let data;
34
85
  if (contentType.includes('application/json')) {
@@ -39,7 +90,6 @@ export class GoogleDriveStorage {
39
90
  console.error('Unexpected Response Type:', text);
40
91
  throw new Error(`Expected JSON response but got: ${contentType}`);
41
92
  }
42
- // Handle non-200 HTTP responses
43
93
  if (!res.ok) {
44
94
  console.error('Error Response:', JSON.stringify(data));
45
95
  throw new Error(data?.error?.message || 'Unknown error occurred');
@@ -54,11 +104,7 @@ export class GoogleDriveStorage {
54
104
  async getFileContent(fileId) {
55
105
  const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`;
56
106
  try {
57
- const response = await this.fetcher({
58
- method: 'GET',
59
- headers: {}, // Add additional headers if required
60
- url,
61
- });
107
+ const response = await this.fetcher({ method: 'GET', headers: {}, url });
62
108
  return response;
63
109
  }
64
110
  catch (error) {
@@ -72,11 +118,7 @@ export class GoogleDriveStorage {
72
118
  headers: {},
73
119
  url: `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&trashed=false&fields=files(id,name,mimeType,parents)`,
74
120
  });
75
- if (!result.files) {
76
- console.error('No files found:', result);
77
- return [];
78
- }
79
- return result.files;
121
+ return result.files || [];
80
122
  }
81
123
  async createFolder({ folderName, parentFolderId }) {
82
124
  if (!parentFolderId) {
@@ -85,7 +127,7 @@ export class GoogleDriveStorage {
85
127
  const metadata = {
86
128
  name: folderName,
87
129
  mimeType: 'application/vnd.google-apps.folder',
88
- parents: [parentFolderId], // Explicitly associate with the parent folder
130
+ parents: [parentFolderId],
89
131
  };
90
132
  const folder = await this.fetcher({
91
133
  method: 'POST',
@@ -94,110 +136,107 @@ export class GoogleDriveStorage {
94
136
  url: 'https://www.googleapis.com/drive/v3/files',
95
137
  });
96
138
  console.log(`Folder created: "${folderName}" with ID: ${folder.id}, Parent: ${parentFolderId}`);
139
+ await this.fetcher({
140
+ method: 'POST',
141
+ url: `https://www.googleapis.com/drive/v3/files/${folder.id}/permissions`,
142
+ headers: {},
143
+ body: JSON.stringify({ role: 'reader', type: 'anyone' }),
144
+ });
97
145
  return folder;
98
146
  }
147
+ async getMediaFolderId() {
148
+ if (this.folderCache['MEDIAs']) {
149
+ return this.folderCache['MEDIAs'];
150
+ }
151
+ const rootFolders = await this.findFolders();
152
+ let credentialsFolder = rootFolders.find((f) => f.name === 'Credentials');
153
+ if (!credentialsFolder) {
154
+ credentialsFolder = await this.createFolder({ folderName: 'Credentials', parentFolderId: 'root' });
155
+ }
156
+ const credentialsFolderId = credentialsFolder.id;
157
+ const subfolders = await this.findFolders(credentialsFolder.id);
158
+ let mediasFolder = subfolders.find((f) => f.name === 'MEDIAs');
159
+ if (!mediasFolder) {
160
+ mediasFolder = await this.createFolder({ folderName: 'MEDIAs', parentFolderId: credentialsFolderId });
161
+ }
162
+ const mediasFolderId = mediasFolder.id;
163
+ this.folderCache['MEDIAs'] = mediasFolderId;
164
+ return mediasFolderId;
165
+ }
166
+ async uploadBinaryFile({ file }) {
167
+ try {
168
+ const accessToken = this.accessToken; // Ensure access token is available
169
+ if (!accessToken) {
170
+ throw new Error('Missing Google OAuth access token.');
171
+ }
172
+ const folderId = await this.getMediaFolderId(); // Ensure folderId is correct
173
+ // ✅ Correct metadata for Google Drive API
174
+ const metadata = {
175
+ name: file.name,
176
+ mimeType: file.type,
177
+ parents: [folderId], // Store in the correct folder
178
+ };
179
+ // ✅ Create FormData for multipart upload
180
+ const formData = new FormData();
181
+ formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
182
+ formData.append('file', file);
183
+ // ✅ Correct Google Drive Upload URL
184
+ const url = `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,parents`;
185
+ const response = await fetch(url, {
186
+ method: 'POST',
187
+ headers: {
188
+ Authorization: `Bearer ${accessToken}`, // ✅ Include valid OAuth token
189
+ },
190
+ body: formData,
191
+ });
192
+ const data = await response.json();
193
+ if (!response.ok) {
194
+ throw new Error(`Google Drive Upload Error: ${data.error?.message || 'Unknown error'}`);
195
+ }
196
+ console.log('✅ File uploaded successfully:', data);
197
+ return data; // Returns the uploaded file ID and parents
198
+ }
199
+ catch (error) {
200
+ console.error('❌ Error uploading file to Google Drive:', error);
201
+ throw error;
202
+ }
203
+ }
99
204
  async saveFile({ data, folderId }) {
205
+ console.log('🚀 ~ GoogleDriveStorage ~ saveFile ~ data:', data);
100
206
  try {
101
207
  if (!folderId) {
102
208
  throw new Error('Folder ID is required to save a file.');
103
209
  }
104
- // Define file metadata, ensure correct folder is assigned
105
210
  const fileMetadata = {
106
- name: data.fileName || 'resume.json', // Use the provided fileName or default to 'resume.json'
107
- parents: [folderId], // Specify the folder ID
108
- mimeType: 'application/json', // Ensure the MIME type is set to JSON
211
+ name: data.fileName || 'resume.json',
212
+ parents: [folderId],
213
+ mimeType: data.mimeType || 'application/json',
109
214
  };
110
- // Check if the parent folder is in the trash
111
- const folder = await this.fetcher({
112
- method: 'GET',
113
- headers: {},
114
- url: `https://www.googleapis.com/drive/v3/files/${folderId}?fields=trashed`,
115
- });
116
- if (folder.trashed) {
117
- throw new Error('Parent folder is in trash');
118
- }
119
- // Prepare the file content as a JSON string
120
- const fileContent = JSON.stringify(data);
121
- // Create a Blob from the JSON string
122
- const fileBlob = new Blob([fileContent], { type: 'application/json' });
123
- // Create FormData and append the metadata and file content
215
+ const fileBlob = new Blob([JSON.stringify(data)], { type: 'application/json' });
124
216
  const formData = new FormData();
125
217
  formData.append('metadata', new Blob([JSON.stringify(fileMetadata)], { type: 'application/json' }));
126
218
  formData.append('file', fileBlob);
127
- // Upload file to Google Drive
128
- console.log('Uploading file...');
129
219
  const file = await this.fetcher({
130
220
  method: 'POST',
131
221
  headers: {},
132
222
  body: formData,
133
223
  url: `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,parents`,
134
224
  });
135
- // Set file permissions
225
+ // Set public read permissions
136
226
  await this.fetcher({
137
227
  method: 'POST',
138
228
  url: `https://www.googleapis.com/drive/v3/files/${file.id}/permissions`,
139
- headers: {},
229
+ headers: { 'Content-Type': 'application/json' },
140
230
  body: JSON.stringify({
141
231
  role: 'reader',
142
232
  type: 'anyone',
143
233
  }),
144
234
  });
145
- // Check for existing file_ids.json in appDataFolder
146
- let existingFileId = null;
147
- let existingFileIds = [];
148
- try {
149
- const existingFileQuery = await this.fetcher({
150
- method: 'GET',
151
- headers: {},
152
- url: `https://www.googleapis.com/drive/v3/files?spaces=appDataFolder&q=name='file_ids.json'&fields=files(id)`,
153
- });
154
- if (existingFileQuery.files?.length > 0) {
155
- existingFileId = existingFileQuery.files[0].id;
156
- const fileContent = await this.fetcher({
157
- method: 'GET',
158
- headers: {},
159
- url: `https://www.googleapis.com/drive/v3/files/${existingFileId}?alt=media`,
160
- });
161
- existingFileIds = JSON.parse(fileContent);
162
- }
163
- console.log('existingFileId', existingFileId);
164
- }
165
- catch (error) {
166
- console.log('Creating new file_ids.json');
167
- }
168
- // Add new file ID
169
- existingFileIds.push(file.id);
170
- // Metadata for app data file
171
- const appDataFileMetadata = {
172
- name: 'file_ids.json',
173
- mimeType: 'application/json',
174
- spaces: ['appDataFolder'],
175
- };
176
- // Update or create file_ids.json
177
- const formDataForAppData = new FormData();
178
- formDataForAppData.append('metadata', new Blob([JSON.stringify(appDataFileMetadata)], { type: 'application/json' }));
179
- formDataForAppData.append('file', new Blob([JSON.stringify(existingFileIds)], { type: 'application/json' }));
180
- if (existingFileId) {
181
- await this.fetcher({
182
- method: 'PATCH',
183
- headers: {},
184
- body: formDataForAppData,
185
- url: `https://www.googleapis.com/upload/drive/v3/files/${existingFileId}?uploadType=multipart`,
186
- });
187
- }
188
- else {
189
- await this.fetcher({
190
- method: 'POST',
191
- headers: {},
192
- body: formDataForAppData,
193
- url: `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&spaces=appDataFolder`,
194
- });
195
- }
196
- console.log(file);
235
+ console.log(`File uploaded successfully: ${file.id}`);
197
236
  return file;
198
237
  }
199
238
  catch (error) {
200
- console.error('Error:', error.message);
239
+ console.error('Error in saveFile:', error);
201
240
  throw error;
202
241
  }
203
242
  }
@@ -207,10 +246,8 @@ export class GoogleDriveStorage {
207
246
  * @returns file content
208
247
  */
209
248
  async retrieve(id) {
210
- const metadataUrl = `https://www.googleapis.com/drive/v3/files/${id}?fields=id,name`;
211
249
  const dataUrl = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`;
212
250
  try {
213
- // Fetch actual file data
214
251
  const dataResponse = await fetch(dataUrl, {
215
252
  method: 'GET',
216
253
  headers: {
@@ -223,7 +260,6 @@ export class GoogleDriveStorage {
223
260
  return null;
224
261
  }
225
262
  const contentType = dataResponse.headers.get('Content-Type');
226
- console.log(`File content type: ${contentType}`);
227
263
  let fileData;
228
264
  if (contentType?.includes('application/json')) {
229
265
  fileData = await dataResponse.json();
@@ -242,7 +278,7 @@ export class GoogleDriveStorage {
242
278
  else {
243
279
  fileData = await dataResponse.arrayBuffer();
244
280
  }
245
- return { data: fileData };
281
+ return { data: fileData, id: id };
246
282
  }
247
283
  catch (error) {
248
284
  console.error(`Error retrieving file with ID ${id}:`, error.message);
@@ -254,13 +290,18 @@ export class GoogleDriveStorage {
254
290
  * @param folderId [Optional]
255
291
  * @returns
256
292
  */
257
- findFolders = async (folderId) => {
293
+ async findFolders(folderId) {
294
+ const cacheKey = folderId || 'root';
295
+ if (this.folderCache[cacheKey]) {
296
+ return this.folderCache[cacheKey];
297
+ }
258
298
  const query = folderId
259
299
  ? `'${folderId}' in parents and mimeType='application/vnd.google-apps.folder'`
260
300
  : `'root' in parents and mimeType='application/vnd.google-apps.folder'`;
261
301
  const folders = await this.searchFiles(query);
262
- return folders.filter((file) => file.mimeType === 'application/vnd.google-apps.folder');
263
- };
302
+ this.folderCache[cacheKey] = folders;
303
+ return folders;
304
+ }
264
305
  /**
265
306
  * Get all files content for the specified type ('KEYPAIRs' | 'VCs' | 'SESSIONs' | 'DIDs' | 'RECOMMENDATIONs')
266
307
  * @param type
@@ -268,58 +309,56 @@ export class GoogleDriveStorage {
268
309
  */
269
310
  async getAllFilesByType(type) {
270
311
  try {
271
- // Step 1: Find the root 'Credentials' folder
272
- const rootFolders = await this.findFolders();
273
- const credentialsFolder = rootFolders.find((f) => f.name === 'Credentials');
312
+ if (!this.folderCache['Credentials']) {
313
+ const rootFolders = await this.findFolders();
314
+ this.folderCache['Credentials'] = rootFolders;
315
+ }
316
+ const credentialsFolder = this.folderCache['Credentials'].find((f) => f.name === 'Credentials');
274
317
  if (!credentialsFolder) {
275
318
  console.error('Credentials folder not found.');
276
319
  return [];
277
320
  }
278
- const credentialsFolderId = credentialsFolder.id;
279
- // Step 2: Handle special case for 'VCs'
280
321
  if (type === 'VCs') {
281
- // Find the 'VCs' folder under 'Credentials'
282
- const subfolders = await this.findFolders(credentialsFolderId);
283
- const targetFolder = subfolders.find((f) => f.name === 'VCs');
284
- if (!targetFolder) {
285
- console.error(`Folder for type ${type} not found.`);
322
+ if (!this.folderCache['VCs']) {
323
+ const vcSubfolder = await this.findFolders(credentialsFolder.id);
324
+ const vcsFolder = vcSubfolder.find((f) => f.name === 'VCs');
325
+ const vcSubFolders = await this.findFolders(vcsFolder.id);
326
+ this.folderCache['VCs'] = vcSubFolders.filter((folder) => folder.name.startsWith('VC-'));
327
+ }
328
+ const vcSubfolders = this.folderCache['VCs'];
329
+ if (!vcSubfolders.length) {
330
+ console.error(`No subfolders found for type: ${type}`);
286
331
  return [];
287
332
  }
288
- const targetFolderId = targetFolder.id;
289
- // Fetch all 'VC-timestamp' subfolders under 'VCs'
290
- const vcSubfolders = await this.findFolders(targetFolderId);
291
- // Retrieve all 'VC.json' files from each 'VC-timestamp' subfolder
292
- const fileContents = await Promise.all(vcSubfolders.map(async (folder) => {
293
- const files = await this.findFilesUnderFolder(folder.id);
294
- console.log('🚀 ~ GoogleDriveStorage ~ vcSubfolders.map ~ files:', files);
295
- return Promise.all(files.map(async (file) => {
296
- return await this.retrieve(file.id);
297
- }));
298
- }));
299
- console.log('🚀 ~ GoogleDriveStorage ~ getAllFilesByType ~ fileContents:', fileContents);
300
- return fileContents;
333
+ const allFilesNested = await Promise.all(vcSubfolders.map(async (folder) => await this.findFilesUnderFolder(folder.id)));
334
+ const allVcJsonFiles = allFilesNested.flat().filter((file) => file.mimeType === 'application/json');
335
+ const fileContentsResults = await Promise.allSettled(allVcJsonFiles.map((file) => this.retrieve(file.id)));
336
+ const validFileContents = fileContentsResults.filter((result) => result.status === 'fulfilled').map((result) => result.value);
337
+ return validFileContents.filter((file) => file.data.fileName !== 'RELATIONS');
301
338
  }
302
- // Step 3: Generic handling for other types
303
- const subfolders = await this.findFolders(credentialsFolderId);
304
- const targetFolder = subfolders.find((f) => f.name === type);
305
- if (!targetFolder) {
339
+ if (!this.folderCache[type]) {
340
+ const subfolders = await this.findFolders(credentialsFolder.id);
341
+ const targetFolder = subfolders.find((f) => f.name === type);
342
+ this.folderCache[type] = targetFolder ? targetFolder.id : null;
343
+ }
344
+ const targetFolderId = this.folderCache[type];
345
+ if (!targetFolderId) {
306
346
  console.error(`Folder for type ${type} not found.`);
307
347
  return [];
308
348
  }
309
349
  const filesResponse = await this.fetcher({
310
350
  method: 'GET',
311
351
  headers: {},
312
- url: `https://www.googleapis.com/drive/v3/files?q='${targetFolder.id}' in parents and trashed=false&fields=files(id,name,mimeType,parents)`,
352
+ url: `https://www.googleapis.com/drive/v3/files?q='${targetFolderId}' in parents and trashed=false&fields=files(id,name,mimeType)`,
313
353
  });
314
- const files = filesResponse.files;
315
- const fileContents = await Promise.all(files.map(async (file) => {
316
- return await this.retrieve(file.id);
317
- }));
318
- return fileContents;
354
+ const files = filesResponse.files || [];
355
+ const fileContents = await Promise.allSettled(files.map((file) => this.retrieve(file.id)));
356
+ console.log('🚀 ~ GoogleDriveStorage ~ getAllFilesByType ~ fileContents:', fileContents);
357
+ return fileContents.filter((res) => res.status === 'fulfilled').map((res) => res.value);
319
358
  }
320
359
  catch (error) {
321
360
  console.error(`Error getting files of type ${type}:`, error);
322
- return []; // Return an empty array on error
361
+ return [];
323
362
  }
324
363
  }
325
364
  /**
@@ -330,9 +369,7 @@ export class GoogleDriveStorage {
330
369
  */
331
370
  async updateFileName(fileId, newFileName) {
332
371
  try {
333
- const metadata = {
334
- name: newFileName, // New name for the file
335
- };
372
+ const metadata = { name: newFileName };
336
373
  const updatedFile = await this.fetcher({
337
374
  method: 'PATCH',
338
375
  headers: { 'Content-Type': 'application/json' },
@@ -348,7 +385,6 @@ export class GoogleDriveStorage {
348
385
  }
349
386
  }
350
387
  async findFileByName(name) {
351
- // find the file named under Credentials folder
352
388
  const rootFolders = await this.findFolders();
353
389
  const credentialsFolderId = rootFolders.find((f) => f.name === 'Credentials')?.id;
354
390
  if (!credentialsFolderId)
@@ -364,7 +400,6 @@ export class GoogleDriveStorage {
364
400
  console.log('No files found in the folder.');
365
401
  return [];
366
402
  }
367
- // Fetch content for each file
368
403
  const filesWithContent = await Promise.all(files.map(async (file) => {
369
404
  try {
370
405
  const content = await this.getFileContent(file.id);
@@ -372,7 +407,7 @@ export class GoogleDriveStorage {
372
407
  }
373
408
  catch (error) {
374
409
  console.error(`Error fetching content for file "${file.name}" (ID: ${file.id}):`, error);
375
- return { ...file, content: null }; // Handle errors gracefully
410
+ return { ...file, content: null };
376
411
  }
377
412
  }));
378
413
  return filesWithContent;
@@ -382,12 +417,8 @@ export class GoogleDriveStorage {
382
417
  const updateUrl = `https://www.googleapis.com/drive/v3/files/${fileId}`;
383
418
  const updatedFile = await this.fetcher({
384
419
  method: 'PATCH',
385
- headers: {
386
- 'Content-Type': 'application/json',
387
- },
388
- body: JSON.stringify({
389
- name: data.fileName,
390
- }),
420
+ headers: { 'Content-Type': 'application/json' },
421
+ body: JSON.stringify({ name: data.fileName }),
391
422
  url: updateUrl,
392
423
  });
393
424
  console.log('✅ File renamed successfully:', updatedFile);
@@ -398,44 +429,50 @@ export class GoogleDriveStorage {
398
429
  }
399
430
  }
400
431
  async getFileParents(fileId) {
401
- console.log('🚀 ~ GoogleDriveStorage ~ getFileParents ~ fileId', fileId);
402
432
  const file = await this.fetcher({
403
433
  method: 'GET',
404
434
  headers: {},
405
435
  url: `https://www.googleapis.com/drive/v3/files/${fileId}?fields=parents`,
406
436
  });
437
+ console.log('FILE: ', file);
407
438
  return file.parents;
408
439
  }
409
440
  async updateRelationsFile({ relationsFileId, recommendationFileId }) {
410
441
  const relationsFileContent = await this.retrieve(relationsFileId);
411
- const relationsData = relationsFileContent.data;
442
+ const relationsData = relationsFileContent.data.body ? JSON.parse(relationsFileContent.data.body) : relationsFileContent.data;
412
443
  relationsData.recommendations.push(recommendationFileId);
413
444
  const updatedContent = JSON.stringify(relationsData);
414
445
  const updateResponse = await this.fetcher({
415
446
  method: 'PATCH',
416
- headers: {
417
- 'Content-Type': 'application/json',
418
- },
447
+ headers: { 'Content-Type': 'application/json' },
419
448
  body: updatedContent,
420
449
  url: `https://www.googleapis.com/upload/drive/v3/files/${relationsFileId}?uploadType=media`,
421
450
  });
422
- console.log('🚀 ~ GoogleDriveStorage ~ updateRelationsFile ~ updateResponse:', updateResponse);
451
+ this.updateFileIdsJson(relationsFileId);
423
452
  return updateResponse;
424
453
  }
425
454
  async createRelationsFile({ vcFolderId }) {
426
455
  const files = await this.findFilesUnderFolder(vcFolderId);
427
456
  const vcFile = files.find((file) => file.name === 'VC');
457
+ const vcContent = await this.getFileContent(vcFile.id);
458
+ console.log('🚀 ~ GoogleDriveStorage ~ createRelationsFile ~ vcContent:', vcContent);
459
+ const subject = JSON.parse(vcContent.body).credentialSubject;
460
+ console.log('🚀 ~ GoogleDriveStorage ~ createRelationsFile ~ subject:', subject);
428
461
  const relationsFile = await this.saveFile({
429
462
  data: {
430
463
  fileName: 'RELATIONS',
431
464
  mimeType: 'application/json',
432
465
  body: JSON.stringify({
433
- vc_id: vcFile.id,
466
+ vc: {
467
+ fileId: vcContent.id,
468
+ subject,
469
+ },
434
470
  recommendations: [],
435
471
  }),
436
472
  },
437
473
  folderId: vcFolderId,
438
474
  });
475
+ await this.updateFileIdsJson(relationsFile.id);
439
476
  return relationsFile;
440
477
  }
441
478
  /**
@@ -459,25 +496,19 @@ export class GoogleDriveStorage {
459
496
  }
460
497
  }
461
498
  async update(fileId, data) {
462
- console.log('🚀 ~ GoogleDriveStorage ~ update ~ data:', data);
463
- console.log('🚀 ~ GoogleDriveStorage ~ update ~ fileId:', fileId);
464
- // ✅ Ensure JSON file type
465
499
  const metadata = {
466
500
  name: data.fileName || 'resume.json',
467
501
  mimeType: 'application/json',
468
502
  };
469
503
  const uploadUrl = `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`;
470
- // ✅ Create multipart request to update Google Drive JSON file
471
504
  const formData = new FormData();
472
505
  formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
473
- formData.append('file', new Blob([JSON.stringify(data.body)], { type: 'application/json' }) // ✅ Ensure JSON format
474
- );
475
- console.log('🚀 ~ GoogleDriveStorage ~ update ~ FormData:', formData);
506
+ formData.append('file', new Blob([JSON.stringify(data.body)], { type: 'application/json' }));
476
507
  try {
477
508
  const response = await this.fetcher({
478
509
  method: 'PATCH',
479
- headers: {}, // ✅ No Content-Type needed, let FormData set it
480
- body: formData, // ✅ Sends JSON file properly
510
+ headers: {},
511
+ body: formData,
481
512
  url: `${uploadUrl}&fields=id,name,mimeType`,
482
513
  });
483
514
  console.log('✅ File updated successfully:', response);
@@ -490,12 +521,22 @@ export class GoogleDriveStorage {
490
521
  }
491
522
  async getFileIdsFromAppDataFolder() {
492
523
  try {
524
+ const constructUrl = () => {
525
+ const baseUrl = 'https://www.googleapis.com/drive/v3/files';
526
+ const queryParams = new URLSearchParams({
527
+ spaces: 'appDataFolder',
528
+ q: "name='file_ids.json'",
529
+ fields: 'files(id)',
530
+ });
531
+ return `${baseUrl}?${queryParams.toString()}`;
532
+ };
493
533
  // Step 1: Search for the file_ids.json file in the appDataFolder
494
534
  const response = await this.fetcher({
495
535
  method: 'GET',
496
536
  headers: {},
497
- url: `https://www.googleapis.com/drive/v3/files?q=name='file_ids.json' and 'appDataFolder' in parents&fields=files(id)`,
537
+ url: constructUrl(),
498
538
  });
539
+ console.log(': GoogleDriveStorage getFileIdsFromAppDataFolder response', response);
499
540
  // Step 2: Check if the file exists
500
541
  if (!response.files || response.files.length === 0) {
501
542
  console.log('No file_ids.json found in appDataFolder.');
@@ -503,14 +544,17 @@ export class GoogleDriveStorage {
503
544
  }
504
545
  // Step 3: Get the file ID of file_ids.json
505
546
  const fileId = response.files[0].id;
547
+ console.log(': GoogleDriveStorage getFileIdsFromAppDataFolder fileId', fileId);
506
548
  // Step 4: Fetch the content of file_ids.json
507
549
  const fileContent = await this.fetcher({
508
550
  method: 'GET',
509
551
  headers: {},
510
552
  url: `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`,
511
553
  });
554
+ console.log(': GoogleDriveStorage getFileIdsFromAppDataFolder fileContent', fileContent);
512
555
  // Step 5: Parse the file content (array of file IDs)
513
- const fileIds = JSON.parse(fileContent);
556
+ const fileIds = fileContent;
557
+ console.log(': GoogleDriveStorage getFileIdsFromAppDataFolder fileIds', fileIds);
514
558
  return fileIds;
515
559
  }
516
560
  catch (error) {
@@ -30,30 +30,33 @@ export class ResumeVC {
30
30
  return unsignedCredential;
31
31
  }
32
32
  generateUnsignedCredential({ formData, issuerDid }) {
33
- const unsignedCredential = {
33
+ const unsignedResumeVC = {
34
34
  '@context': [
35
- 'https://www.w3.org/2018/credentials/v1', // Standard VC context
35
+ 'https://www.w3.org/2018/credentials/v1',
36
+ 'https://schema.hropenstandards.org/4.4/context.jsonld',
36
37
  inlineResumeContext['@context'], // Inline context
37
38
  ],
38
- id: `urn:uuid:${uuidv4()}`, // Generate a dynamic UUID
39
- type: ['VerifiableCredential'],
39
+ id: `urn:uuid:${uuidv4()}`, // Generate a unique UUID
40
+ type: ['VerifiableCredential', 'LERRSCredential'], // LER-RS compliant credential type
40
41
  issuer: issuerDid,
41
- issuanceDate: new Date().toISOString(),
42
+ issuanceDate: new Date().toISOString(), // Current date/time in ISO format
42
43
  credentialSubject: {
43
44
  type: 'Resume',
44
45
  person: {
45
46
  name: {
46
- formattedName: formData.formattedName,
47
+ formattedName: formData.formattedName || '',
47
48
  },
48
- primaryLanguage: formData.primaryLanguage,
49
+ primaryLanguage: formData.primaryLanguage || 'en',
49
50
  },
50
- narrative: formData.narrative,
51
- employmentHistory: formData.employmentHistory,
52
- skills: formData.skills,
53
- educationAndLearning: formData.educationAndLearning,
51
+ narrative: {
52
+ text: formData.narrative || 'Narrative text goes here',
53
+ },
54
+ employmentHistory: formData.employmentHistory || [],
55
+ skills: formData.skills || [],
56
+ educationAndLearning: formData.educationAndLearning || {},
54
57
  },
55
58
  };
56
- return unsignedCredential;
59
+ return unsignedResumeVC;
57
60
  }
58
61
  generateKeyPair = async (address) => {
59
62
  // Generate the key pair using the library's method
@@ -1,4 +1,5 @@
1
1
  import { DidDocument, KeyPair, FormDataI, RecommendationFormDataI, VerifiableCredential } from '../../types/credential.js';
2
+ import { GoogleDriveStorage } from './GoogleDriveStorage.js';
2
3
  interface SignPropsI {
3
4
  data: FormDataI | RecommendationFormDataI;
4
5
  type: 'VC' | 'RECOMMENDATION';
@@ -9,7 +10,6 @@ interface SignPropsI {
9
10
  /**
10
11
  * Class representing the Credential Engine.
11
12
  * @class CredentialEngine
12
- * @param {string} accessToken - The access token for the user.
13
13
  * @classdesc Credential Engine class to create DIDs and VCs.
14
14
  * @method createDID - Create a new DID with Digital Bazaar's Ed25519VerificationKey2020 key pair.
15
15
  * @method createWalletDID - Create a new DID with user metamask address as controller.
@@ -21,7 +21,7 @@ interface SignPropsI {
21
21
  export declare class CredentialEngine {
22
22
  private storage;
23
23
  private keyPair;
24
- constructor(accessToken: string);
24
+ constructor(storage: GoogleDriveStorage);
25
25
  private getKeyPair;
26
26
  private generateKeyPair;
27
27
  private verifyCreds;
@@ -1,8 +1,4 @@
1
- interface FileContent {
2
- name: string;
3
- content: any;
4
- comments: string[];
5
- }
1
+ type FileType = 'KEYPAIRs' | 'VCs' | 'SESSIONs' | 'DIDs' | 'RECOMMENDATIONs' | 'MEDIAs';
6
2
  /**
7
3
  * @class GoogleDriveStorage
8
4
  * @description Class to interact with Google Drive API
@@ -20,6 +16,9 @@ interface FileContent {
20
16
  */
21
17
  export declare class GoogleDriveStorage {
22
18
  private accessToken;
19
+ folderCache: any;
20
+ private fileIdsCache;
21
+ private updateFileIdsJson;
23
22
  constructor(accessToken: string);
24
23
  private fetcher;
25
24
  private getFileContent;
@@ -27,12 +26,11 @@ export declare class GoogleDriveStorage {
27
26
  createFolder({ folderName, parentFolderId }: {
28
27
  folderName: string;
29
28
  parentFolderId: string;
30
- }): Promise<{
31
- id: string;
32
- name: string;
33
- mimeType: string;
34
- parents: string[];
35
- }>;
29
+ }): Promise<any>;
30
+ getMediaFolderId(): Promise<any>;
31
+ uploadBinaryFile({ file }: {
32
+ file: File;
33
+ }): Promise<any>;
36
34
  saveFile({ data, folderId }: {
37
35
  data: any;
38
36
  folderId: string;
@@ -44,19 +42,20 @@ export declare class GoogleDriveStorage {
44
42
  */
45
43
  retrieve(id: string): Promise<{
46
44
  data: any;
45
+ id: string;
47
46
  } | null>;
48
47
  /**
49
48
  * Get folder by folderId, if folderId == null you will have them all
50
49
  * @param folderId [Optional]
51
50
  * @returns
52
51
  */
53
- findFolders: (folderId?: string) => Promise<any[]>;
52
+ findFolders(folderId?: string): Promise<any[]>;
54
53
  /**
55
54
  * Get all files content for the specified type ('KEYPAIRs' | 'VCs' | 'SESSIONs' | 'DIDs' | 'RECOMMENDATIONs')
56
55
  * @param type
57
56
  * @returns
58
57
  */
59
- getAllFilesByType(type: 'KEYPAIRs' | 'VCs' | 'SESSIONs' | 'DIDs' | 'RECOMMENDATIONs' | 'MEDIAs'): Promise<FileContent[]>;
58
+ getAllFilesByType(type: FileType): Promise<any[]>;
60
59
  /**
61
60
  * Update the name of a file in Google Drive
62
61
  * @param fileId - The ID of the file to update
@@ -4,7 +4,10 @@ export declare class ResumeVC {
4
4
  issuerDid: string;
5
5
  keyPair: any;
6
6
  }): Promise<any>;
7
- private generateUnsignedCredential;
7
+ generateUnsignedCredential({ formData, issuerDid }: {
8
+ formData: any;
9
+ issuerDid: string;
10
+ }): any;
8
11
  generateKeyPair: (address?: string) => Promise<any>;
9
12
  /**
10
13
  * Create a new DID with Digital Bazaar's Ed25519VerificationKey2020 key pair.
@@ -29,8 +29,8 @@ export declare function generateUnsignedVC({ formData, issuerDid }: {
29
29
  * @returns {RecommendationCredential} The created unsigned Recommendation Credential.
30
30
  * @throws Will throw an error if the recommendation creation fails or if issuance date exceeds expiration date.
31
31
  */
32
- export declare function generateUnsignedRecommendation({ vc, recommendation, issuerDid, }: {
33
- vc: any;
32
+ export declare function generateUnsignedRecommendation({ vcId, recommendation, issuerDid, }: {
33
+ vcId: string;
34
34
  recommendation: RecommendationFormDataI;
35
35
  issuerDid: string;
36
36
  }): RecommendationCredential;
@@ -12,8 +12,9 @@ export declare const getVCWithRecommendations: ({ vcId, storage }: {
12
12
  }) => Promise<{
13
13
  vc: {
14
14
  data: any;
15
+ id: string;
15
16
  };
16
- recommendations: any[];
17
+ recommendationIds: any;
17
18
  relationsFileId: any;
18
19
  }>;
19
20
  /**
@@ -26,13 +27,14 @@ export declare const getVCWithRecommendations: ({ vcId, storage }: {
26
27
  */
27
28
  export declare function saveToGoogleDrive({ storage, data, type }: SaveToGooglePropsI): Promise<any>;
28
29
  /**
29
- * Upload an image to Google Drive in the Credentials/MEDIAs folder.
30
+ * Upload any type of file to Google Drive in the Credentials/MEDIAs folder.
30
31
  * @param {GoogleDriveStorage} storage - The GoogleDriveStorage instance.
31
- * @param {File} imageFile - The image file to upload.
32
- * @returns {Promise<>} - The uploaded image file object.
32
+ * @param {File} file - The file to upload.
33
+ * @param {string} folderName - The name of the folder where the file will be saved (default is 'MEDIAs').
34
+ * @returns {Promise<{ id: string }>} - The uploaded file object.
33
35
  * @throws Will throw an error if the upload operation fails.
34
36
  */
35
- export declare function uploadImageToGoogleDrive(storage: GoogleDriveStorage, imageFile: File): Promise<{
37
+ export declare function uploadToGoogleDrive(storage: GoogleDriveStorage, file: File, folderName?: string): Promise<{
36
38
  id: string;
37
39
  }>;
38
40
  export declare function generateViewLink(fileId: string): string;
@@ -131,10 +131,7 @@ export function generateUnsignedVC({ formData, issuerDid }) {
131
131
  * @returns {RecommendationCredential} The created unsigned Recommendation Credential.
132
132
  * @throws Will throw an error if the recommendation creation fails or if issuance date exceeds expiration date.
133
133
  */
134
- export function generateUnsignedRecommendation({ vc, recommendation, issuerDid, }) {
135
- console.log('🚀 ~ vc.id:', vc.id);
136
- console.log('🚀 ~ vc:', vc);
137
- console.log('🚀 ~ recommendation:', recommendation);
134
+ export function generateUnsignedRecommendation({ vcId, recommendation, issuerDid, }) {
138
135
  const issuanceDate = new Date().toISOString();
139
136
  if (issuanceDate > recommendation.expirationDate)
140
137
  throw new Error('issuanceDate cannot be after expirationDate');
@@ -150,7 +147,7 @@ export function generateUnsignedRecommendation({ vc, recommendation, issuerDid,
150
147
  portfolio: 'https://schema.org/portfolio',
151
148
  },
152
149
  ],
153
- id: '', // Will be set after hashing VC
150
+ id: `urn:${generateHashedId({ id: vcId })}`,
154
151
  type: ['VerifiableCredential', 'https://schema.org/RecommendationCredential'],
155
152
  issuer: {
156
153
  id: issuerDid,
@@ -170,8 +167,6 @@ export function generateUnsignedRecommendation({ vc, recommendation, issuerDid,
170
167
  })),
171
168
  },
172
169
  };
173
- // Use the VC's hashed ID for the Recommendation's ID
174
- unsignedRecommendation.id = vc.data.id;
175
170
  return unsignedRecommendation;
176
171
  }
177
172
  /**
@@ -1,16 +1,18 @@
1
1
  export const getVCWithRecommendations = async ({ vcId, storage }) => {
2
- const vcFolderId = await storage.getFileParents(vcId);
3
- const files = await storage.findFilesUnderFolder(vcFolderId);
4
- const relationsFile = files.find((f) => f.name === 'RELATIONS');
5
- const relationsContent = await storage.retrieve(relationsFile.id);
6
- const relationsData = relationsContent.data;
7
- const [vcFileId, recommendationIds] = [relationsData.vc_id, relationsData.recommendations];
8
- const vc = await storage.retrieve(vcFileId);
9
- const recommendations = await Promise.all(recommendationIds.map(async (rec) => {
10
- const recFile = await storage.retrieve(rec);
11
- return recFile;
12
- }));
13
- return { vc: vc, recommendations, relationsFileId: relationsFile.id };
2
+ try {
3
+ const vcFolderId = await storage.getFileParents(vcId);
4
+ const files = await storage.findFilesUnderFolder(vcFolderId);
5
+ const relationsFile = files.find((f) => f.name === 'RELATIONS');
6
+ const relationsContent = await storage.retrieve(relationsFile.id);
7
+ const relationsData = relationsContent.data.body ? JSON.parse(relationsContent.data.body) : relationsContent.data;
8
+ const recommendationIds = relationsData.recommendations || [];
9
+ const vc = await storage.retrieve(vcId);
10
+ return { vc: vc, recommendationIds, relationsFileId: relationsFile.id };
11
+ }
12
+ catch (error) {
13
+ console.error('Error getting VC with recommendations:', error);
14
+ throw error;
15
+ }
14
16
  };
15
17
  /**
16
18
  * Save data to Google Drive in the specified folder type.
@@ -53,7 +55,6 @@ export async function saveToGoogleDrive({ storage, data, type }) {
53
55
  }
54
56
  // Save the file in the specific subfolder
55
57
  const file = await storage.saveFile({ data: fileData, folderId: typeFolderId });
56
- console.log('🚀 ~ file:', file);
57
58
  return file;
58
59
  }
59
60
  catch (error) {
@@ -62,17 +63,19 @@ export async function saveToGoogleDrive({ storage, data, type }) {
62
63
  }
63
64
  }
64
65
  /**
65
- * Upload an image to Google Drive in the Credentials/MEDIAs folder.
66
+ * Upload any type of file to Google Drive in the Credentials/MEDIAs folder.
66
67
  * @param {GoogleDriveStorage} storage - The GoogleDriveStorage instance.
67
- * @param {File} imageFile - The image file to upload.
68
- * @returns {Promise<>} - The uploaded image file object.
68
+ * @param {File} file - The file to upload.
69
+ * @param {string} folderName - The name of the folder where the file will be saved (default is 'MEDIAs').
70
+ * @returns {Promise<{ id: string }>} - The uploaded file object.
69
71
  * @throws Will throw an error if the upload operation fails.
70
72
  */
71
- export async function uploadImageToGoogleDrive(storage, imageFile) {
73
+ export async function uploadToGoogleDrive(storage, file, folderName = 'MEDIAs') {
72
74
  try {
73
75
  const rootFolders = await storage.findFolders();
74
76
  let credentialsFolder = rootFolders.find((f) => f.name === 'Credentials');
75
77
  if (!credentialsFolder) {
78
+ console.log('Creating Credentials folder...');
76
79
  credentialsFolder = await storage.createFolder({ folderName: 'Credentials', parentFolderId: 'root' });
77
80
  }
78
81
  const credentialsFolderId = credentialsFolder.id;
@@ -83,17 +86,16 @@ export async function uploadImageToGoogleDrive(storage, imageFile) {
83
86
  }
84
87
  const mediasFolderId = mediasFolder.id;
85
88
  // Prepare the image file data
86
- const imageData = {
87
- fileName: imageFile.name,
88
- mimeType: imageFile.type,
89
- body: imageFile,
89
+ const fileMetaData = {
90
+ fileName: file.name,
91
+ mimeType: file.type,
92
+ body: file,
90
93
  };
91
94
  // SaveFile the image in the "MEDIAs" folder
92
95
  const uploadedImage = await storage.saveFile({
93
- data: imageData,
96
+ data: fileMetaData,
94
97
  folderId: mediasFolderId,
95
98
  });
96
- console.log('🚀 ~ uploadedImage:', uploadedImage);
97
99
  return uploadedImage;
98
100
  }
99
101
  catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cooperation/vc-storage",
3
3
  "type": "module",
4
- "version": "1.0.21",
4
+ "version": "1.0.23",
5
5
  "description": "Sign and store your verifiable credentials.",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/types/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "test": "jest",
13
+ "test": "vitest",
14
14
  "version": "npm version patch"
15
15
  },
16
16
  "author": "cooperation",
@@ -22,26 +22,18 @@
22
22
  "@digitalbazaar/vc": "^6.3.0",
23
23
  "crypto-js": "^4.2.0",
24
24
  "ethers": "^6.13.2",
25
+ "jest": "^29.7.0",
26
+ "ts-jest": "^29.2.5",
25
27
  "ts-node": "^10.9.2",
26
28
  "tsc": "^2.0.4",
27
29
  "uuid": "^10.0.0"
28
30
  },
29
31
  "devDependencies": {
30
32
  "@types/fs-extra": "^11.0.4",
31
- "@types/jest": "^29.5.12",
33
+ "@types/jest": "^29.5.14",
32
34
  "@types/uuid": "^10.0.0",
33
- "typescript": "^5.6.2"
34
- },
35
- "jest": {
36
- "preset": "ts-jest",
37
- "testEnvironment": "node",
38
- "moduleFileExtensions": [
39
- "ts",
40
- "tsx",
41
- "js"
42
- ],
43
- "roots": [
44
- "<rootDir>/tests"
45
- ]
35
+ "babel-jest": "^29.7.0",
36
+ "typescript": "^5.6.2",
37
+ "vitest": "^3.0.5"
46
38
  }
47
39
  }