@cooperation/vc-storage 1.0.11 → 1.0.13
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/models/CredentialEngine.js +48 -18
- package/dist/models/GoogleDriveStorage.js +151 -131
- package/dist/types/models/CredentialEngine.d.ts +16 -3
- package/dist/types/models/GoogleDriveStorage.d.ts +18 -19
- package/dist/types/utils/credential.d.ts +29 -10
- package/dist/types/utils/google.d.ts +23 -5
- package/dist/utils/credential.js +131 -109
- package/dist/utils/google.js +31 -20
- package/dist/utils/presentation.js +26 -29
- package/package.json +2 -1
- package/dist/types/utils/saveToGoogle.d.ts +0 -10
- package/dist/utils/saveToGoogle.js +0 -65
@@ -1,6 +1,6 @@
|
|
1
1
|
import { Ed25519VerificationKey2020 } from '@digitalbazaar/ed25519-verification-key-2020';
|
2
2
|
import { Ed25519Signature2020 } from '@digitalbazaar/ed25519-signature-2020';
|
3
|
-
import * as
|
3
|
+
import * as dbVc from '@digitalbazaar/vc';
|
4
4
|
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';
|
@@ -19,11 +19,9 @@ import { GoogleDriveStorage } from './GoogleDriveStorage.js';
|
|
19
19
|
* @method signPresentation - Sign a Verifiable Presentation (VP).
|
20
20
|
*/
|
21
21
|
export class CredentialEngine {
|
22
|
-
uuid;
|
23
22
|
storage;
|
24
23
|
keyPair;
|
25
24
|
constructor(accessToken) {
|
26
|
-
this.uuid = uuidv4();
|
27
25
|
this.storage = new GoogleDriveStorage(accessToken);
|
28
26
|
}
|
29
27
|
async getKeyPair(vc) {
|
@@ -75,8 +73,11 @@ export class CredentialEngine {
|
|
75
73
|
async createDID() {
|
76
74
|
try {
|
77
75
|
const keyPair = await this.generateKeyPair();
|
78
|
-
const keyFile = await saveToGoogleDrive(
|
79
|
-
|
76
|
+
const keyFile = await saveToGoogleDrive({
|
77
|
+
storage: this.storage,
|
78
|
+
data: keyPair,
|
79
|
+
type: 'KEYPAIR',
|
80
|
+
});
|
80
81
|
const didDocument = await generateDIDSchema(keyPair);
|
81
82
|
return { didDocument, keyPair };
|
82
83
|
}
|
@@ -85,6 +86,18 @@ export class CredentialEngine {
|
|
85
86
|
throw error;
|
86
87
|
}
|
87
88
|
}
|
89
|
+
async findKeysAndDIDs() {
|
90
|
+
const keyPairs = (await this.storage.getAllFilesByType('KEYPAIRs'));
|
91
|
+
const DIDs = (await this.storage.getAllFilesByType('DIDs'));
|
92
|
+
if (DIDs.length === 0 || keyPairs.length === 0)
|
93
|
+
return null;
|
94
|
+
const keyPair = keyPairs[0].data;
|
95
|
+
const didDocument = DIDs[0].data.didDocument;
|
96
|
+
return {
|
97
|
+
didDocument,
|
98
|
+
keyPair,
|
99
|
+
};
|
100
|
+
}
|
88
101
|
/**
|
89
102
|
* Create a new DID with user metamask address as controller
|
90
103
|
* @param walletrAddress
|
@@ -94,7 +107,11 @@ export class CredentialEngine {
|
|
94
107
|
async createWalletDID(walletrAddress) {
|
95
108
|
try {
|
96
109
|
const keyPair = await this.generateKeyPair(walletrAddress);
|
97
|
-
const keyFile = await saveToGoogleDrive(
|
110
|
+
const keyFile = await saveToGoogleDrive({
|
111
|
+
storage: this.storage,
|
112
|
+
data: keyPair,
|
113
|
+
type: 'KEYPAIR',
|
114
|
+
});
|
98
115
|
console.log('🚀 ~ CredentialEngine ~ createWalletDID ~ keyFile:', keyFile);
|
99
116
|
const didDocument = await generateDIDSchema(keyPair);
|
100
117
|
return { didDocument, keyPair };
|
@@ -109,20 +126,33 @@ export class CredentialEngine {
|
|
109
126
|
* @param {'VC' | 'RECOMMENDATION'} type - The signature type.
|
110
127
|
* @param {string} issuerId - The ID of the issuer [currently we put it as the did id]
|
111
128
|
* @param {KeyPair} keyPair - The key pair to use for signing.
|
129
|
+
* @param {FormDataI | RecommendationFormDataI} formData - The form data to include in the VC.
|
130
|
+
* @param {string} VCId - The ID of the credential when the type is RECOMMENDATION
|
112
131
|
* @returns {Promise<Credential>} The signed VC.
|
113
132
|
* @throws Will throw an error if VC signing fails.
|
114
133
|
*/
|
115
|
-
async signVC(
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
134
|
+
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
|
+
let credential = generateUnsignedVC({ formData: data, issuerDid: issuerId });
|
144
|
+
if (type == 'RECOMMENDATION' && vcFileId) {
|
145
|
+
console.log('WOW');
|
146
|
+
vc = (await this.storage.retrieve(vcFileId));
|
147
|
+
credential = generateUnsignedRecommendation({ vc, recommendation: data, issuerDid: issuerId });
|
122
148
|
}
|
123
|
-
const suite = new Ed25519Signature2020({ key: keyPair, verificationMethod: keyPair.id });
|
124
149
|
try {
|
125
|
-
|
150
|
+
console.log('🚀 ~ CredentialEngine ~ signVC ~ credential:', credential);
|
151
|
+
if (!credential)
|
152
|
+
throw new Error('Invalid credential type');
|
153
|
+
const suite = new Ed25519Signature2020({ key: keyPair, verificationMethod: keyPair.id });
|
154
|
+
console.log('before');
|
155
|
+
const signedVC = await dbVc.issue({ credential, suite, documentLoader: customDocumentLoader });
|
126
156
|
return signedVC;
|
127
157
|
}
|
128
158
|
catch (error) {
|
@@ -143,7 +173,7 @@ export class CredentialEngine {
|
|
143
173
|
key: keyPair,
|
144
174
|
verificationMethod: keyPair.id,
|
145
175
|
});
|
146
|
-
const result = await
|
176
|
+
const result = await dbVc.verifyCredential({
|
147
177
|
credential,
|
148
178
|
suite,
|
149
179
|
documentLoader: customDocumentLoader,
|
@@ -169,7 +199,7 @@ export class CredentialEngine {
|
|
169
199
|
const id = `urn:uuid:${uuidv4()}`;
|
170
200
|
const keyPair = await this.getKeyPair(verifiableCredential[0]);
|
171
201
|
console.log('🚀 ~ CredentialEngine ~ createPresentation ~ keyPair:', keyPair);
|
172
|
-
const VP = await
|
202
|
+
const VP = await dbVc.createPresentation({ verifiableCredential, id, holder: keyPair.controller });
|
173
203
|
return VP;
|
174
204
|
}
|
175
205
|
catch (error) {
|
@@ -198,7 +228,7 @@ export class CredentialEngine {
|
|
198
228
|
verificationMethod: this.keyPair.id,
|
199
229
|
});
|
200
230
|
// Sign the presentation
|
201
|
-
const signedVP = await
|
231
|
+
const signedVP = await dbVc.signPresentation({
|
202
232
|
presentation,
|
203
233
|
suite,
|
204
234
|
documentLoader: customDocumentLoader,
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import { generateViewLink } from '../utils/google.js';
|
2
1
|
/**
|
3
2
|
* @class GoogleDriveStorage
|
4
3
|
* @description Class to interact with Google Drive API
|
@@ -19,7 +18,6 @@ export class GoogleDriveStorage {
|
|
19
18
|
constructor(accessToken) {
|
20
19
|
this.accessToken = accessToken;
|
21
20
|
}
|
22
|
-
// Method to fetch data from Google Drive API
|
23
21
|
async fetcher({ method, headers, body, url }) {
|
24
22
|
try {
|
25
23
|
const res = await fetch(url, {
|
@@ -30,26 +28,39 @@ export class GoogleDriveStorage {
|
|
30
28
|
}),
|
31
29
|
body,
|
32
30
|
});
|
33
|
-
// Check
|
34
|
-
const
|
31
|
+
// Check the Content-Type to ensure it's JSON before parsing
|
32
|
+
const contentType = res.headers.get('Content-Type') || '';
|
33
|
+
let data;
|
34
|
+
if (contentType.includes('application/json')) {
|
35
|
+
data = await res.json();
|
36
|
+
}
|
37
|
+
else {
|
38
|
+
const text = await res.text();
|
39
|
+
console.error('Unexpected Response Type:', text);
|
40
|
+
throw new Error(`Expected JSON response but got: ${contentType}`);
|
41
|
+
}
|
42
|
+
// Handle non-200 HTTP responses
|
35
43
|
if (!res.ok) {
|
36
44
|
console.error('Error Response:', JSON.stringify(data));
|
37
|
-
throw new Error(data
|
45
|
+
throw new Error(data?.error?.message || 'Unknown error occurred');
|
38
46
|
}
|
39
47
|
return data;
|
40
48
|
}
|
41
49
|
catch (error) {
|
42
|
-
console.error('Error fetching data:', error.message);
|
50
|
+
console.error('Error fetching data:', error.message || error);
|
43
51
|
throw error;
|
44
52
|
}
|
45
53
|
}
|
46
|
-
// Method to search for files in Google Drive by query
|
47
54
|
async searchFiles(query) {
|
48
55
|
const result = await this.fetcher({
|
49
56
|
method: 'GET',
|
50
57
|
headers: {},
|
51
58
|
url: `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&trashed=false&fields=files(id,name,mimeType,parents)`,
|
52
59
|
});
|
60
|
+
if (!result.files) {
|
61
|
+
console.error('No files found:', result);
|
62
|
+
return [];
|
63
|
+
}
|
53
64
|
return result.files;
|
54
65
|
}
|
55
66
|
async createFolder(folderName, parentFolderId) {
|
@@ -67,7 +78,7 @@ export class GoogleDriveStorage {
|
|
67
78
|
console.log('Folder ID:', folder.id);
|
68
79
|
return folder.id;
|
69
80
|
}
|
70
|
-
async
|
81
|
+
async saveFile({ data, folderId }) {
|
71
82
|
try {
|
72
83
|
// Define file metadata, ensure correct folder is assigned
|
73
84
|
const fileMetadata = {
|
@@ -84,9 +95,8 @@ export class GoogleDriveStorage {
|
|
84
95
|
method: 'POST',
|
85
96
|
headers: {},
|
86
97
|
body: formData,
|
87
|
-
url: uploadUrl,
|
98
|
+
url: `${uploadUrl}&fields=id,parents`, // Request the file ID and parent folder IDs
|
88
99
|
});
|
89
|
-
console.log('File uploaded successfully:', file.id);
|
90
100
|
// Set the file permission to "Anyone with the link" can view
|
91
101
|
const permissionUrl = `https://www.googleapis.com/drive/v3/files/${file.id}/permissions`;
|
92
102
|
const permissionData = {
|
@@ -96,112 +106,42 @@ export class GoogleDriveStorage {
|
|
96
106
|
await this.fetcher({
|
97
107
|
method: 'POST',
|
98
108
|
url: permissionUrl,
|
99
|
-
headers: {
|
100
|
-
'Content-Type': 'application/json',
|
101
|
-
},
|
109
|
+
headers: {},
|
102
110
|
body: JSON.stringify(permissionData),
|
103
111
|
});
|
104
112
|
console.log('Permission set to public for file:', file.id);
|
105
|
-
|
113
|
+
console.log('Parent folder IDs:', file.parents);
|
114
|
+
return file;
|
106
115
|
}
|
107
116
|
catch (error) {
|
108
117
|
console.error('Error uploading file or setting permission:', error.message);
|
109
118
|
return null;
|
110
119
|
}
|
111
120
|
}
|
112
|
-
/**
|
113
|
-
* Add comment to VC
|
114
|
-
* @param fileId - th id of VC file
|
115
|
-
* @returns
|
116
|
-
*/
|
117
|
-
async addCommentToFile(vcFileId, recommendationFileId) {
|
118
|
-
if (!recommendationFileId || !vcFileId || !this.accessToken) {
|
119
|
-
throw new Error('Missing required parameters: fileId, commentText, or accessToken');
|
120
|
-
}
|
121
|
-
const url = `https://www.googleapis.com/drive/v3/files/${vcFileId}/comments?fields=id,content,createdTime`;
|
122
|
-
const body = {
|
123
|
-
content: generateViewLink(recommendationFileId),
|
124
|
-
};
|
125
|
-
try {
|
126
|
-
const response = await fetch(url, {
|
127
|
-
method: 'POST',
|
128
|
-
headers: {
|
129
|
-
Authorization: `Bearer ${this.accessToken}`,
|
130
|
-
'Content-Type': 'application/json',
|
131
|
-
},
|
132
|
-
body: JSON.stringify(body),
|
133
|
-
});
|
134
|
-
if (!response.ok) {
|
135
|
-
const errorDetails = await response.json();
|
136
|
-
throw new Error(`Failed to add comment: ${JSON.stringify(errorDetails)}`);
|
137
|
-
}
|
138
|
-
const result = await response.json();
|
139
|
-
console.log('Comment added successfully:', result);
|
140
|
-
return result;
|
141
|
-
}
|
142
|
-
catch (error) {
|
143
|
-
console.error('Error adding comment to file:', error);
|
144
|
-
throw error;
|
145
|
-
}
|
146
|
-
}
|
147
|
-
/**
|
148
|
-
* Add commenter role to a file
|
149
|
-
* @param fileId
|
150
|
-
* @returns
|
151
|
-
*/
|
152
|
-
async addCommenterRoleToFile(fileId) {
|
153
|
-
const url = `https://www.googleapis.com/drive/v3/files/${fileId}/permissions`;
|
154
|
-
const body = {
|
155
|
-
role: 'commenter',
|
156
|
-
type: 'anyone',
|
157
|
-
};
|
158
|
-
try {
|
159
|
-
const response = await fetch(url, {
|
160
|
-
method: 'POST',
|
161
|
-
headers: {
|
162
|
-
Authorization: `Bearer ${this.accessToken}`,
|
163
|
-
'Content-Type': 'application/json',
|
164
|
-
},
|
165
|
-
body: JSON.stringify(body),
|
166
|
-
});
|
167
|
-
if (!response.ok) {
|
168
|
-
const errorDetails = await response.json();
|
169
|
-
throw new Error(`Failed to add permission: ${JSON.stringify(errorDetails)}`);
|
170
|
-
}
|
171
|
-
const result = await response.json();
|
172
|
-
console.log('Permission added successfully:', result);
|
173
|
-
return result;
|
174
|
-
}
|
175
|
-
catch (error) {
|
176
|
-
console.error('Error adding permission:', error.message);
|
177
|
-
throw error;
|
178
|
-
}
|
179
|
-
}
|
180
121
|
/**
|
181
122
|
* Get file from google drive by id
|
182
123
|
* @param id
|
183
124
|
* @returns file content
|
184
125
|
*/
|
185
126
|
async retrieve(id) {
|
186
|
-
const metadataUrl = `https://www.googleapis.com/drive/v3/files/${id}?fields=name`;
|
127
|
+
const metadataUrl = `https://www.googleapis.com/drive/v3/files/${id}?fields=id,name`;
|
187
128
|
const dataUrl = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`;
|
188
129
|
try {
|
189
|
-
|
190
|
-
|
191
|
-
const metadataResponse = await fetch(metadataUrl, {
|
130
|
+
// Initial "touch" request to ensure file accessibility for the current user
|
131
|
+
const touchResponse = await fetch(metadataUrl, {
|
192
132
|
method: 'GET',
|
193
133
|
headers: {
|
194
134
|
Authorization: `Bearer ${this.accessToken}`,
|
195
135
|
},
|
196
136
|
});
|
197
|
-
if (!
|
198
|
-
const errorData = await
|
199
|
-
console.error(`Failed to
|
137
|
+
if (!touchResponse.ok) {
|
138
|
+
const errorData = await touchResponse.json();
|
139
|
+
console.error(`Failed to "touch" file for accessibility with ID ${id}:`, errorData);
|
200
140
|
return null;
|
201
141
|
}
|
202
|
-
|
142
|
+
// Fetch file metadata to get the name
|
143
|
+
const metadata = await touchResponse.json();
|
203
144
|
const fileName = metadata.name;
|
204
|
-
console.log(`File name retrieved: ${fileName}`);
|
205
145
|
// Fetch actual file data
|
206
146
|
const dataResponse = await fetch(dataUrl, {
|
207
147
|
method: 'GET',
|
@@ -237,9 +177,8 @@ export class GoogleDriveStorage {
|
|
237
177
|
else {
|
238
178
|
fileData = await dataResponse.arrayBuffer(); // Fallback for other binary types
|
239
179
|
}
|
240
|
-
console.log('🚀 ~ GoogleDriveStorage ~ retrieve ~ fileData:', fileData);
|
241
180
|
// Return file ID, name, and data
|
242
|
-
return { id, name: fileName, data: fileData };
|
181
|
+
return { id: metadata.id, name: fileName, data: fileData };
|
243
182
|
}
|
244
183
|
catch (error) {
|
245
184
|
console.error(`Error retrieving file with ID ${id}:`, error.message);
|
@@ -248,7 +187,7 @@ export class GoogleDriveStorage {
|
|
248
187
|
}
|
249
188
|
/**
|
250
189
|
* Get folder by folderId, if folderId == null you will have them all
|
251
|
-
* @param
|
190
|
+
* @param folderId [Optional]
|
252
191
|
* @returns
|
253
192
|
*/
|
254
193
|
findFolders = async (folderId) => {
|
@@ -310,22 +249,6 @@ export class GoogleDriveStorage {
|
|
310
249
|
return null;
|
311
250
|
}
|
312
251
|
};
|
313
|
-
async getFileComments(fileId) {
|
314
|
-
try {
|
315
|
-
// Fetch comments on the file using Google Drive API
|
316
|
-
const commentsResponse = await this.fetcher({
|
317
|
-
method: 'GET',
|
318
|
-
headers: {},
|
319
|
-
url: `https://www.googleapis.com/drive/v3/files/${fileId}/comments?fields=comments(content,author/displayName,createdTime)`,
|
320
|
-
});
|
321
|
-
// Return the comments data if available
|
322
|
-
return commentsResponse.comments || []; // Return an empty array if no comments
|
323
|
-
}
|
324
|
-
catch (error) {
|
325
|
-
console.error(`Failed to fetch comments for file ID: ${fileId}`, error);
|
326
|
-
return []; // Handle errors by returning an empty array or some error indication
|
327
|
-
}
|
328
|
-
}
|
329
252
|
/**
|
330
253
|
* Get all files content for the specified type ('KEYPAIRs' | 'VCs' | 'SESSIONs' | 'DIDs' | 'RECOMMENDATIONs')
|
331
254
|
* @param type
|
@@ -333,42 +256,64 @@ export class GoogleDriveStorage {
|
|
333
256
|
*/
|
334
257
|
async getAllFilesByType(type) {
|
335
258
|
try {
|
336
|
-
// Step 1: Find
|
259
|
+
// Step 1: Find the root 'Credentials' folder
|
337
260
|
const rootFolders = await this.findFolders();
|
338
261
|
const credentialsFolder = rootFolders.find((f) => f.name === 'Credentials');
|
339
|
-
if (!credentialsFolder)
|
262
|
+
if (!credentialsFolder) {
|
263
|
+
console.error('Credentials folder not found.');
|
340
264
|
return [];
|
265
|
+
}
|
341
266
|
const credentialsFolderId = credentialsFolder.id;
|
342
|
-
// Step 2:
|
267
|
+
// Step 2: Handle special case for 'VCs'
|
268
|
+
if (type === 'VCs') {
|
269
|
+
// Find the 'VCs' folder under 'Credentials'
|
270
|
+
const subfolders = await this.findFolders(credentialsFolderId);
|
271
|
+
const targetFolder = subfolders.find((f) => f.name === 'VCs');
|
272
|
+
if (!targetFolder) {
|
273
|
+
console.error(`Folder for type ${type} not found.`);
|
274
|
+
return [];
|
275
|
+
}
|
276
|
+
const targetFolderId = targetFolder.id;
|
277
|
+
// Fetch all 'VC-timestamp' subfolders under 'VCs'
|
278
|
+
const vcSubfolders = await this.findFolders(targetFolderId);
|
279
|
+
// Retrieve all 'VC.json' files from each 'VC-timestamp' subfolder
|
280
|
+
const fileContents = await Promise.all(vcSubfolders.map(async (folder) => {
|
281
|
+
const files = await this.findFilesUnderFolder(folder.id);
|
282
|
+
return Promise.all(files.map(async (file) => {
|
283
|
+
return await this.retrieve(file.id);
|
284
|
+
}));
|
285
|
+
}));
|
286
|
+
// for (const folder of vcSubfolders) {
|
287
|
+
// const files = await this.findFilesUnderFolder(folder.id);
|
288
|
+
// // Fetch the content of each file
|
289
|
+
// for (const file of files) {
|
290
|
+
// try {
|
291
|
+
// const content = await this.retrieve(file.id);
|
292
|
+
// fileContents.push(content);
|
293
|
+
// } catch (error) {
|
294
|
+
// console.error(`Error retrieving content for file ${file.id}:`, error);
|
295
|
+
// }
|
296
|
+
// }
|
297
|
+
// }
|
298
|
+
return fileContents;
|
299
|
+
}
|
300
|
+
// Step 3: Generic handling for other types
|
343
301
|
const subfolders = await this.findFolders(credentialsFolderId);
|
344
302
|
const targetFolder = subfolders.find((f) => f.name === type);
|
345
|
-
if (!targetFolder)
|
303
|
+
if (!targetFolder) {
|
304
|
+
console.error(`Folder for type ${type} not found.`);
|
346
305
|
return [];
|
347
|
-
|
306
|
+
}
|
348
307
|
const filesResponse = await this.fetcher({
|
349
308
|
method: 'GET',
|
350
309
|
headers: {},
|
351
310
|
url: `https://www.googleapis.com/drive/v3/files?q='${targetFolder.id}' in parents and trashed=false&fields=files(id,name,mimeType,parents)`,
|
352
311
|
});
|
353
312
|
const files = filesResponse.files;
|
354
|
-
// Step 4: Fetch the content and comments of each file
|
355
313
|
const fileContents = await Promise.all(files.map(async (file) => {
|
356
|
-
|
357
|
-
const content = await this.fetcher({
|
358
|
-
method: 'GET',
|
359
|
-
headers: {},
|
360
|
-
url: `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`,
|
361
|
-
});
|
362
|
-
// Fetch file comments (if applicable)
|
363
|
-
const comments = await this.getFileComments(file.id);
|
364
|
-
return {
|
365
|
-
id: file.id,
|
366
|
-
name: file.name,
|
367
|
-
content,
|
368
|
-
comments: comments.map((comment) => comment.content),
|
369
|
-
};
|
314
|
+
return await this.retrieve(file.id);
|
370
315
|
}));
|
371
|
-
return fileContents;
|
316
|
+
return fileContents;
|
372
317
|
}
|
373
318
|
catch (error) {
|
374
319
|
console.error(`Error getting files of type ${type}:`, error);
|
@@ -400,6 +345,81 @@ export class GoogleDriveStorage {
|
|
400
345
|
throw error;
|
401
346
|
}
|
402
347
|
}
|
348
|
+
async findFileByName(name) {
|
349
|
+
// find the file named under Credentials folder
|
350
|
+
const rootFolders = await this.findFolders();
|
351
|
+
const credentialsFolderId = rootFolders.find((f) => f.name === 'Credentials')?.id;
|
352
|
+
if (!credentialsFolderId)
|
353
|
+
throw new Error('Credentials folder not found');
|
354
|
+
const files = await this.searchFiles(`'${credentialsFolderId}' in parents and name='${name}'`);
|
355
|
+
return files[0];
|
356
|
+
}
|
357
|
+
async findFilesUnderFolder(folderId) {
|
358
|
+
if (!folderId)
|
359
|
+
throw new Error('Folder ID is required');
|
360
|
+
console.log('🚀 ~ GoogleDriveStorage ~ findFilesUnderFolder ~ folderId', folderId);
|
361
|
+
const files = await this.searchFiles(`'${folderId}' in parents`);
|
362
|
+
return files;
|
363
|
+
}
|
364
|
+
async updateFileData(fileId, data) {
|
365
|
+
const fileMetadata = {
|
366
|
+
name: data.fileName,
|
367
|
+
mimeType: data.mimeType,
|
368
|
+
};
|
369
|
+
let uploadUrl = `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`;
|
370
|
+
const formData = new FormData();
|
371
|
+
formData.append('metadata', new Blob([JSON.stringify(fileMetadata)], { type: 'application/json' }));
|
372
|
+
formData.append('file', new Blob([data.body], { type: fileMetadata.mimeType }));
|
373
|
+
const updatedFile = await this.fetcher({
|
374
|
+
method: 'PATCH',
|
375
|
+
headers: {},
|
376
|
+
body: JSON.stringify(formData),
|
377
|
+
url: `${uploadUrl}&fields=id,parents`,
|
378
|
+
});
|
379
|
+
console.log('File updated:', updatedFile);
|
380
|
+
return updatedFile;
|
381
|
+
}
|
382
|
+
async getFileParents(fileId) {
|
383
|
+
console.log('🚀 ~ GoogleDriveStorage ~ getFileParents ~ fileId', fileId);
|
384
|
+
const file = await this.fetcher({
|
385
|
+
method: 'GET',
|
386
|
+
headers: {},
|
387
|
+
url: `https://www.googleapis.com/drive/v3/files/${fileId}?fields=parents`,
|
388
|
+
});
|
389
|
+
return file.parents;
|
390
|
+
}
|
391
|
+
async updateRelationsFile({ relationsFileId, recommendationFileId }) {
|
392
|
+
const relationsFileContent = await this.retrieve(relationsFileId);
|
393
|
+
const relationsData = relationsFileContent.data;
|
394
|
+
relationsData.recommendations.push(recommendationFileId);
|
395
|
+
const updatedContent = JSON.stringify(relationsData);
|
396
|
+
const updateResponse = await this.fetcher({
|
397
|
+
method: 'PATCH',
|
398
|
+
headers: {
|
399
|
+
'Content-Type': 'application/json',
|
400
|
+
},
|
401
|
+
body: updatedContent,
|
402
|
+
url: `https://www.googleapis.com/upload/drive/v3/files/${relationsFileId}?uploadType=media`,
|
403
|
+
});
|
404
|
+
console.log('🚀 ~ GoogleDriveStorage ~ updateRelationsFile ~ updateResponse:', updateResponse);
|
405
|
+
return updateResponse;
|
406
|
+
}
|
407
|
+
async createRelationsFile({ vcFolderId }) {
|
408
|
+
const files = await this.findFilesUnderFolder(vcFolderId);
|
409
|
+
const vcFile = files.find((file) => file.name === 'VC');
|
410
|
+
const relationsFile = await this.saveFile({
|
411
|
+
data: {
|
412
|
+
fileName: 'RELATIONS',
|
413
|
+
mimeType: 'application/json',
|
414
|
+
body: JSON.stringify({
|
415
|
+
vc_id: vcFile.id,
|
416
|
+
recommendations: [],
|
417
|
+
}),
|
418
|
+
},
|
419
|
+
folderId: vcFolderId,
|
420
|
+
});
|
421
|
+
return relationsFile;
|
422
|
+
}
|
403
423
|
/**
|
404
424
|
* Delete file by id
|
405
425
|
* @param id
|
@@ -1,4 +1,11 @@
|
|
1
|
-
import { DidDocument, KeyPair, VerifiableCredential } from '../../types/credential.js';
|
1
|
+
import { DidDocument, KeyPair, FormDataI, RecommendationFormDataI, VerifiableCredential } from '../../types/credential.js';
|
2
|
+
interface SignPropsI {
|
3
|
+
data: FormDataI | RecommendationFormDataI;
|
4
|
+
type: 'VC' | 'RECOMMENDATION';
|
5
|
+
keyPair: KeyPair;
|
6
|
+
issuerId: string;
|
7
|
+
vcFileId?: string;
|
8
|
+
}
|
2
9
|
/**
|
3
10
|
* Class representing the Credential Engine.
|
4
11
|
* @class CredentialEngine
|
@@ -12,7 +19,6 @@ import { DidDocument, KeyPair, VerifiableCredential } from '../../types/credenti
|
|
12
19
|
* @method signPresentation - Sign a Verifiable Presentation (VP).
|
13
20
|
*/
|
14
21
|
export declare class CredentialEngine {
|
15
|
-
private uuid;
|
16
22
|
private storage;
|
17
23
|
private keyPair;
|
18
24
|
constructor(accessToken: string);
|
@@ -28,6 +34,10 @@ export declare class CredentialEngine {
|
|
28
34
|
didDocument: DidDocument;
|
29
35
|
keyPair: KeyPair;
|
30
36
|
}>;
|
37
|
+
findKeysAndDIDs(): Promise<{
|
38
|
+
didDocument: any;
|
39
|
+
keyPair: any;
|
40
|
+
}>;
|
31
41
|
/**
|
32
42
|
* Create a new DID with user metamask address as controller
|
33
43
|
* @param walletrAddress
|
@@ -43,10 +53,12 @@ export declare class CredentialEngine {
|
|
43
53
|
* @param {'VC' | 'RECOMMENDATION'} type - The signature type.
|
44
54
|
* @param {string} issuerId - The ID of the issuer [currently we put it as the did id]
|
45
55
|
* @param {KeyPair} keyPair - The key pair to use for signing.
|
56
|
+
* @param {FormDataI | RecommendationFormDataI} formData - The form data to include in the VC.
|
57
|
+
* @param {string} VCId - The ID of the credential when the type is RECOMMENDATION
|
46
58
|
* @returns {Promise<Credential>} The signed VC.
|
47
59
|
* @throws Will throw an error if VC signing fails.
|
48
60
|
*/
|
49
|
-
signVC(
|
61
|
+
signVC({ data, type, keyPair, issuerId, vcFileId }: SignPropsI): Promise<any>;
|
50
62
|
/**
|
51
63
|
* Verify a Verifiable Credential (VC)
|
52
64
|
* @param {object} credential - The Verifiable Credential to verify.
|
@@ -67,3 +79,4 @@ export declare class CredentialEngine {
|
|
67
79
|
*/
|
68
80
|
signPresentation(presentation: any): Promise<any>;
|
69
81
|
}
|
82
|
+
export {};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { DataToSaveI } from '../../types';
|
1
|
+
import { DataToSaveI, FilesType } from '../../types';
|
2
2
|
interface FileContent {
|
3
3
|
name: string;
|
4
4
|
content: any;
|
@@ -25,34 +25,23 @@ export declare class GoogleDriveStorage {
|
|
25
25
|
private fetcher;
|
26
26
|
private searchFiles;
|
27
27
|
createFolder(folderName: string, parentFolderId?: string): Promise<string>;
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
* Add comment to VC
|
33
|
-
* @param fileId - th id of VC file
|
34
|
-
* @returns
|
35
|
-
*/
|
36
|
-
addCommentToFile(vcFileId: string, recommendationFileId: string): Promise<any>;
|
37
|
-
/**
|
38
|
-
* Add commenter role to a file
|
39
|
-
* @param fileId
|
40
|
-
* @returns
|
41
|
-
*/
|
42
|
-
addCommenterRoleToFile(fileId: string): Promise<any>;
|
28
|
+
saveFile({ data, folderId }: {
|
29
|
+
data: DataToSaveI;
|
30
|
+
folderId: string;
|
31
|
+
}): Promise<any>;
|
43
32
|
/**
|
44
33
|
* Get file from google drive by id
|
45
34
|
* @param id
|
46
35
|
* @returns file content
|
47
36
|
*/
|
48
37
|
retrieve(id: string): Promise<{
|
49
|
-
id: string;
|
50
38
|
name: string;
|
51
39
|
data: any;
|
40
|
+
id: string;
|
52
41
|
} | null>;
|
53
42
|
/**
|
54
43
|
* Get folder by folderId, if folderId == null you will have them all
|
55
|
-
* @param
|
44
|
+
* @param folderId [Optional]
|
56
45
|
* @returns
|
57
46
|
*/
|
58
47
|
findFolders: (folderId?: string) => Promise<any[]>;
|
@@ -62,7 +51,6 @@ export declare class GoogleDriveStorage {
|
|
62
51
|
* @returns last file content from folder by folderId
|
63
52
|
*/
|
64
53
|
findLastFile: (folderId: string) => Promise<any>;
|
65
|
-
getFileComments(fileId: string): Promise<any>;
|
66
54
|
/**
|
67
55
|
* Get all files content for the specified type ('KEYPAIRs' | 'VCs' | 'SESSIONs' | 'DIDs' | 'RECOMMENDATIONs')
|
68
56
|
* @param type
|
@@ -76,6 +64,17 @@ export declare class GoogleDriveStorage {
|
|
76
64
|
* @returns The updated file metadata, including the new name
|
77
65
|
*/
|
78
66
|
updateFileName(fileId: string, newFileName: string): Promise<any>;
|
67
|
+
findFileByName(name: FilesType): Promise<any>;
|
68
|
+
findFilesUnderFolder(folderId: string): Promise<any[]>;
|
69
|
+
updateFileData(fileId: string, data: DataToSaveI): Promise<any>;
|
70
|
+
getFileParents(fileId: string): Promise<any>;
|
71
|
+
updateRelationsFile({ relationsFileId, recommendationFileId }: {
|
72
|
+
relationsFileId: string;
|
73
|
+
recommendationFileId: string;
|
74
|
+
}): Promise<any>;
|
75
|
+
createRelationsFile({ vcFolderId }: {
|
76
|
+
vcFolderId: string;
|
77
|
+
}): Promise<any>;
|
79
78
|
/**
|
80
79
|
* Delete file by id
|
81
80
|
* @param id
|
@@ -1,20 +1,39 @@
|
|
1
1
|
import { KeyPair, DidDocument, FormDataI, RecommendationCredential, Credential, RecommendationFormDataI, VerifiableCredential } from '../../types/credential';
|
2
2
|
/**
|
3
3
|
* Create a DID document using the provided key pair.
|
4
|
-
* @param {
|
5
|
-
* @returns {Promise<
|
4
|
+
* @param {KeyPair} keyPair - The key pair used to create the DID document.
|
5
|
+
* @returns {Promise<DidDocument>} The created DID document.
|
6
|
+
* @throws Will throw an error if the DID document generation fails.
|
6
7
|
*/
|
7
8
|
export declare const generateDIDSchema: (keyPair: KeyPair) => Promise<DidDocument>;
|
8
9
|
/**
|
9
|
-
* Generate an unsigned Verifiable Credential (VC)
|
10
|
-
*
|
11
|
-
* @param {
|
12
|
-
* @param {string}
|
13
|
-
* @
|
14
|
-
* @
|
10
|
+
* Generate an unsigned Verifiable Credential (VC).
|
11
|
+
* Hashes the credential to create a unique ID.
|
12
|
+
* @param {FormDataI} params
|
13
|
+
* @param {string} params.FormData - The form dta to include in the VC.
|
14
|
+
* @param {string} params.issuerDid - The DID of the issuer.
|
15
|
+
* @returns {Credential} The created unsigned VC.
|
16
|
+
* @throws Will throw an error if the VC creation fails or if issuance date exceeds expiration date.
|
15
17
|
*/
|
16
|
-
export declare function generateUnsignedVC(formData
|
17
|
-
|
18
|
+
export declare function generateUnsignedVC({ formData, issuerDid }: {
|
19
|
+
formData: FormDataI;
|
20
|
+
issuerDid: string;
|
21
|
+
}): Credential;
|
22
|
+
/**
|
23
|
+
* Generate an unsigned Recommendation Credential.
|
24
|
+
* Uses the hash of the VC to set the `id` for consistency.
|
25
|
+
* @param {object} params
|
26
|
+
* @param {VerifiableCredential} params.vc - The Verifiable Credential to base the recommendation on.
|
27
|
+
* @param {RecommendationFormDataI} params.recommendation - The recommendation form data.
|
28
|
+
* @param {string} params.issuerDid - The DID of the issuer.
|
29
|
+
* @returns {RecommendationCredential} The created unsigned Recommendation Credential.
|
30
|
+
* @throws Will throw an error if the recommendation creation fails or if issuance date exceeds expiration date.
|
31
|
+
*/
|
32
|
+
export declare function generateUnsignedRecommendation({ vc, recommendation, issuerDid, }: {
|
33
|
+
vc: any;
|
34
|
+
recommendation: RecommendationFormDataI;
|
35
|
+
issuerDid: string;
|
36
|
+
}): RecommendationCredential;
|
18
37
|
/**
|
19
38
|
* Extracts the keypair from a Verifiable Credential
|
20
39
|
* @param {Object} credential - The signed Verifiable Credential
|
@@ -1,15 +1,32 @@
|
|
1
1
|
import { GoogleDriveStorage } from '../models/GoogleDriveStorage.js';
|
2
|
+
export type FileType = 'VC' | 'DID' | 'SESSION' | 'RECOMMENDATION' | 'KEYPAIR';
|
3
|
+
interface SaveToGooglePropsI {
|
4
|
+
storage: GoogleDriveStorage;
|
5
|
+
data: any;
|
6
|
+
type: FileType;
|
7
|
+
vcId?: string;
|
8
|
+
}
|
9
|
+
export declare const getVCWithRecommendations: ({ vcId, storage }: {
|
10
|
+
vcId: string;
|
11
|
+
storage: GoogleDriveStorage;
|
12
|
+
}) => Promise<{
|
13
|
+
vc: {
|
14
|
+
name: string;
|
15
|
+
data: any;
|
16
|
+
id: string;
|
17
|
+
};
|
18
|
+
recommendations: any[];
|
19
|
+
relationsFileId: any;
|
20
|
+
}>;
|
2
21
|
/**
|
3
|
-
* keyFile name = {uuid}-type-timestamp // we need that
|
4
|
-
* vc.id = urn-uuid-{uuid} // we got that
|
5
22
|
* Save data to Google Drive in the specified folder type.
|
6
23
|
* @param {object} data - The data to save.
|
7
|
-
* @param {
|
24
|
+
* @param {FileType} data.type - The type of data being saved.
|
8
25
|
* @returns {Promise<object>} - The file object saved to Google Drive.
|
9
|
-
* @param {string}
|
26
|
+
* @param {string} data.vcId - Optional unique identifier for the VC to link the recommendations.
|
10
27
|
* @throws Will throw an error if the save operation fails.
|
11
28
|
*/
|
12
|
-
export declare function saveToGoogleDrive(storage
|
29
|
+
export declare function saveToGoogleDrive({ storage, data, type }: SaveToGooglePropsI): Promise<any>;
|
13
30
|
/**
|
14
31
|
* Upload an image to Google Drive in the Credentials/MEDIAs folder.
|
15
32
|
* @param {GoogleDriveStorage} storage - The GoogleDriveStorage instance.
|
@@ -22,3 +39,4 @@ export declare function uploadImageToGoogleDrive(storage: GoogleDriveStorage, im
|
|
22
39
|
}>;
|
23
40
|
export declare function generateViewLink(fileId: string): string;
|
24
41
|
export declare function extractGoogleDriveFileId(url: string): string | null;
|
42
|
+
export {};
|
package/dist/utils/credential.js
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|
import { Ed25519VerificationKey2020 } from '@digitalbazaar/ed25519-verification-key-2020';
|
2
|
+
import crypto from 'crypto';
|
2
3
|
import { v4 as uuidv4 } from 'uuid';
|
4
|
+
/**
|
5
|
+
* Utility function to generate a hashed ID for a credential.
|
6
|
+
* Excludes the `id` field when hashing.
|
7
|
+
* @param {object} credential - The credential object to hash.
|
8
|
+
* @returns {string} The generated hashed ID.
|
9
|
+
*/
|
10
|
+
function generateHashedId(credential) {
|
11
|
+
// Exclude the `id` field from the hash
|
12
|
+
const credentialWithoutId = { ...credential, id: undefined };
|
13
|
+
const serialized = JSON.stringify(credentialWithoutId);
|
14
|
+
return crypto.createHash('sha256').update(serialized).digest('hex');
|
15
|
+
}
|
3
16
|
/**
|
4
17
|
* Create a DID document using the provided key pair.
|
5
|
-
* @param {
|
6
|
-
* @returns {Promise<
|
18
|
+
* @param {KeyPair} keyPair - The key pair used to create the DID document.
|
19
|
+
* @returns {Promise<DidDocument>} The created DID document.
|
20
|
+
* @throws Will throw an error if the DID document generation fails.
|
7
21
|
*/
|
8
22
|
export const generateDIDSchema = async (keyPair) => {
|
9
23
|
try {
|
10
24
|
const DID = keyPair.controller;
|
11
|
-
|
25
|
+
return {
|
12
26
|
'@context': ['https://www.w3.org/ns/did/v1'],
|
13
27
|
id: DID,
|
14
28
|
publicKey: [
|
@@ -32,7 +46,6 @@ export const generateDIDSchema = async (keyPair) => {
|
|
32
46
|
},
|
33
47
|
],
|
34
48
|
};
|
35
|
-
return didDocument;
|
36
49
|
}
|
37
50
|
catch (error) {
|
38
51
|
console.error('Error creating DID document:', error);
|
@@ -40,117 +53,126 @@ export const generateDIDSchema = async (keyPair) => {
|
|
40
53
|
}
|
41
54
|
};
|
42
55
|
/**
|
43
|
-
* Generate an unsigned Verifiable Credential (VC)
|
44
|
-
*
|
45
|
-
* @param {
|
46
|
-
* @param {string}
|
47
|
-
* @
|
48
|
-
* @
|
56
|
+
* Generate an unsigned Verifiable Credential (VC).
|
57
|
+
* Hashes the credential to create a unique ID.
|
58
|
+
* @param {FormDataI} params
|
59
|
+
* @param {string} params.FormData - The form dta to include in the VC.
|
60
|
+
* @param {string} params.issuerDid - The DID of the issuer.
|
61
|
+
* @returns {Credential} The created unsigned VC.
|
62
|
+
* @throws Will throw an error if the VC creation fails or if issuance date exceeds expiration date.
|
49
63
|
*/
|
50
|
-
export function generateUnsignedVC(formData, issuerDid
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
'
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
credentialType: 'https://schema.org/credentialType',
|
66
|
-
},
|
67
|
-
],
|
68
|
-
id: `urn:uuid:${vcId}`, //! i want this uuid to be in the condition where vcId part == keypair file name which is like this `${uuid ? uuid + '_' : ''}${type}_${timestamp}.json`,
|
69
|
-
type: ['VerifiableCredential', 'OpenBadgeCredential'],
|
70
|
-
issuer: {
|
71
|
-
id: issuerDid,
|
72
|
-
type: ['Profile'],
|
64
|
+
export function generateUnsignedVC({ formData, issuerDid }) {
|
65
|
+
const issuanceDate = new Date().toISOString();
|
66
|
+
if (issuanceDate > formData.expirationDate)
|
67
|
+
throw new Error('issuanceDate cannot be after expirationDate');
|
68
|
+
const unsignedCredential = {
|
69
|
+
'@context': [
|
70
|
+
'https://www.w3.org/2018/credentials/v1',
|
71
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
72
|
+
{
|
73
|
+
duration: 'https://schema.org/duration',
|
74
|
+
fullName: 'https://schema.org/name',
|
75
|
+
portfolio: 'https://schema.org/portfolio',
|
76
|
+
evidenceLink: 'https://schema.org/evidenceLink',
|
77
|
+
evidenceDescription: 'https://schema.org/evidenceDescription',
|
78
|
+
credentialType: 'https://schema.org/credentialType',
|
73
79
|
},
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
type: 'Image',
|
97
|
-
}
|
98
|
-
: undefined,
|
99
|
-
},
|
100
|
-
],
|
101
|
-
},
|
102
|
-
};
|
103
|
-
return unsignedCredential;
|
104
|
-
}
|
105
|
-
catch (error) {
|
106
|
-
console.error('Error creating unsigned VC', error);
|
107
|
-
throw error;
|
108
|
-
}
|
109
|
-
}
|
110
|
-
export function generateUnsignedRecommendation(recommendation, issuerDid) {
|
111
|
-
try {
|
112
|
-
const issuanceDate = new Date().toISOString();
|
113
|
-
if (issuanceDate > recommendation.expirationDate)
|
114
|
-
throw Error('issuanceDate cannot be after expirationDate');
|
115
|
-
const unsignedRecommendation = {
|
116
|
-
'@context': [
|
117
|
-
'https://www.w3.org/2018/credentials/v1',
|
118
|
-
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
80
|
+
],
|
81
|
+
id: '', // Will be set after hashing
|
82
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential'],
|
83
|
+
issuer: {
|
84
|
+
id: issuerDid,
|
85
|
+
type: ['Profile'],
|
86
|
+
},
|
87
|
+
issuanceDate,
|
88
|
+
expirationDate: formData.expirationDate,
|
89
|
+
credentialSubject: {
|
90
|
+
type: ['AchievementSubject'],
|
91
|
+
name: formData.fullName,
|
92
|
+
portfolio: formData.portfolio.map((item) => ({
|
93
|
+
'@type': 'schema:CreativeWork',
|
94
|
+
name: item.name,
|
95
|
+
url: item.url,
|
96
|
+
})),
|
97
|
+
evidenceLink: formData.evidenceLink,
|
98
|
+
evidenceDescription: formData.achievementDescription,
|
99
|
+
duration: formData.duration,
|
100
|
+
credentialType: formData.credentialType,
|
101
|
+
achievement: [
|
119
102
|
{
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
103
|
+
id: `urn:uuid:${uuidv4()}`,
|
104
|
+
type: ['Achievement'],
|
105
|
+
criteria: {
|
106
|
+
narrative: formData.criteriaNarrative,
|
107
|
+
},
|
108
|
+
description: formData.achievementDescription,
|
109
|
+
name: formData.achievementName,
|
110
|
+
image: formData.evidenceLink
|
111
|
+
? {
|
112
|
+
id: formData.evidenceLink,
|
113
|
+
type: 'Image',
|
114
|
+
}
|
115
|
+
: undefined,
|
125
116
|
},
|
126
117
|
],
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
118
|
+
},
|
119
|
+
};
|
120
|
+
// Generate the hashed ID
|
121
|
+
unsignedCredential.id = 'urn:' + generateHashedId(unsignedCredential);
|
122
|
+
return unsignedCredential;
|
123
|
+
}
|
124
|
+
/**
|
125
|
+
* Generate an unsigned Recommendation Credential.
|
126
|
+
* Uses the hash of the VC to set the `id` for consistency.
|
127
|
+
* @param {object} params
|
128
|
+
* @param {VerifiableCredential} params.vc - The Verifiable Credential to base the recommendation on.
|
129
|
+
* @param {RecommendationFormDataI} params.recommendation - The recommendation form data.
|
130
|
+
* @param {string} params.issuerDid - The DID of the issuer.
|
131
|
+
* @returns {RecommendationCredential} The created unsigned Recommendation Credential.
|
132
|
+
* @throws Will throw an error if the recommendation creation fails or if issuance date exceeds expiration date.
|
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);
|
138
|
+
const issuanceDate = new Date().toISOString();
|
139
|
+
if (issuanceDate > recommendation.expirationDate)
|
140
|
+
throw new Error('issuanceDate cannot be after expirationDate');
|
141
|
+
const unsignedRecommendation = {
|
142
|
+
'@context': [
|
143
|
+
'https://www.w3.org/2018/credentials/v1',
|
144
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
145
|
+
{
|
146
|
+
howKnow: 'https://schema.org/howKnow',
|
147
|
+
recommendationText: 'https://schema.org/recommendationText',
|
148
|
+
qualifications: 'https://schema.org/qualifications',
|
149
|
+
explainAnswer: 'https://schema.org/explainAnswer',
|
150
|
+
portfolio: 'https://schema.org/portfolio',
|
145
151
|
},
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
152
|
+
],
|
153
|
+
id: '', // Will be set after hashing VC
|
154
|
+
type: ['VerifiableCredential', 'https://schema.org/RecommendationCredential'],
|
155
|
+
issuer: {
|
156
|
+
id: issuerDid,
|
157
|
+
type: ['Profile'],
|
158
|
+
},
|
159
|
+
issuanceDate,
|
160
|
+
expirationDate: recommendation.expirationDate,
|
161
|
+
credentialSubject: {
|
162
|
+
name: recommendation.fullName,
|
163
|
+
howKnow: recommendation.howKnow,
|
164
|
+
recommendationText: recommendation.recommendationText,
|
165
|
+
qualifications: recommendation.qualifications,
|
166
|
+
explainAnswer: recommendation.explainAnswer,
|
167
|
+
portfolio: recommendation.portfolio.map((item) => ({
|
168
|
+
name: item.name,
|
169
|
+
url: item.url,
|
170
|
+
})),
|
171
|
+
},
|
172
|
+
};
|
173
|
+
// Use the VC's hashed ID for the Recommendation's ID
|
174
|
+
unsignedRecommendation.id = vc.data.id;
|
175
|
+
return unsignedRecommendation;
|
154
176
|
}
|
155
177
|
/**
|
156
178
|
* Extracts the keypair from a Verifiable Credential
|
package/dist/utils/google.js
CHANGED
@@ -1,18 +1,29 @@
|
|
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 };
|
14
|
+
};
|
1
15
|
/**
|
2
|
-
* keyFile name = {uuid}-type-timestamp // we need that
|
3
|
-
* vc.id = urn-uuid-{uuid} // we got that
|
4
16
|
* Save data to Google Drive in the specified folder type.
|
5
17
|
* @param {object} data - The data to save.
|
6
|
-
* @param {
|
18
|
+
* @param {FileType} data.type - The type of data being saved.
|
7
19
|
* @returns {Promise<object>} - The file object saved to Google Drive.
|
8
|
-
* @param {string}
|
20
|
+
* @param {string} data.vcId - Optional unique identifier for the VC to link the recommendations.
|
9
21
|
* @throws Will throw an error if the save operation fails.
|
10
22
|
*/
|
11
|
-
export async function saveToGoogleDrive(storage, data, type
|
23
|
+
export async function saveToGoogleDrive({ storage, data, type }) {
|
12
24
|
try {
|
13
|
-
const timestamp = Date.now();
|
14
25
|
const fileData = {
|
15
|
-
fileName:
|
26
|
+
fileName: type === 'VC' ? 'VC' : `${type}-${Date.now()}`,
|
16
27
|
mimeType: 'application/json',
|
17
28
|
body: JSON.stringify(data),
|
18
29
|
};
|
@@ -24,34 +35,31 @@ export async function saveToGoogleDrive(storage, data, type, uuid) {
|
|
24
35
|
let credentialsFolderId;
|
25
36
|
if (!credentialsFolder) {
|
26
37
|
credentialsFolderId = await storage.createFolder('Credentials');
|
27
|
-
console.log('Created Credentials folder with ID:', credentialsFolderId);
|
28
38
|
}
|
29
39
|
else {
|
30
40
|
credentialsFolderId = credentialsFolder.id;
|
31
|
-
console.log('Found Credentials folder with ID:', credentialsFolderId);
|
32
41
|
}
|
33
42
|
// Get subfolders within the "Credentials" folder
|
34
43
|
const subfolders = await storage.findFolders(credentialsFolderId);
|
35
|
-
console.log(`Subfolders in Credentials (ID: ${credentialsFolderId}):`, subfolders);
|
36
44
|
// Find or create the specific subfolder (DIDs or VCs)
|
37
45
|
let typeFolder = subfolders.find((f) => f.name === `${type}s`);
|
38
46
|
let typeFolderId;
|
39
47
|
if (!typeFolder) {
|
40
48
|
typeFolderId = await storage.createFolder(`${type}s`, credentialsFolderId);
|
41
|
-
console.log(`Created ${type}s folder with ID:`, typeFolderId);
|
42
49
|
}
|
43
50
|
else {
|
44
51
|
typeFolderId = typeFolder.id;
|
45
|
-
|
46
|
-
|
52
|
+
}
|
53
|
+
if (type === 'VC') {
|
54
|
+
// save the data in Credentials/VCs/VC-timestamp/vc.json
|
55
|
+
const vcFolderId = await storage.createFolder(`${fileData.fileName}-${Date.now()}`, typeFolderId);
|
56
|
+
const file = await storage.saveFile({ data: fileData, folderId: vcFolderId });
|
57
|
+
console.log(`File uploaded: ${file?.id} under ${fileData.fileName} folder in VCs folder`);
|
58
|
+
return file;
|
47
59
|
}
|
48
60
|
// Save the file in the specific subfolder
|
49
|
-
const file = await storage.
|
61
|
+
const file = await storage.saveFile({ data: fileData, folderId: typeFolderId });
|
50
62
|
console.log(`File uploaded: ${file?.id} under ${type}s with ID ${typeFolderId} folder in Credentials folder`);
|
51
|
-
if (file && file.id) {
|
52
|
-
console.log('Sharing file with second user...');
|
53
|
-
await storage.addCommenterRoleToFile(file.id);
|
54
|
-
}
|
55
63
|
return file;
|
56
64
|
}
|
57
65
|
catch (error) {
|
@@ -92,8 +100,11 @@ export async function uploadImageToGoogleDrive(storage, imageFile) {
|
|
92
100
|
mimeType: imageFile.type,
|
93
101
|
body: imageFile,
|
94
102
|
};
|
95
|
-
//
|
96
|
-
const uploadedImage = await storage.
|
103
|
+
// SaveFile the image in the "MEDIAs" folder
|
104
|
+
const uploadedImage = await storage.saveFile({
|
105
|
+
data: imageData,
|
106
|
+
folderId: mediasFolderId,
|
107
|
+
});
|
97
108
|
console.log(`Image uploaded: ${uploadedImage?.id} to MEDIAs folder in Credentials`);
|
98
109
|
return uploadedImage;
|
99
110
|
}
|
@@ -1,6 +1,3 @@
|
|
1
|
-
import { CredentialEngine } from '../models/CredentialEngine.js';
|
2
|
-
import { GoogleDriveStorage } from '../models/GoogleDriveStorage.js';
|
3
|
-
import { extractGoogleDriveFileId } from './google.js';
|
4
1
|
/**
|
5
2
|
* Create and sign a Verifiable Presentation (VP) from a given Verifiable Credential (VC) file and any associated recommendations.
|
6
3
|
* @param {string} accessTokens - The access tokens for the user.
|
@@ -14,32 +11,32 @@ export const createAndSignVerifiablePresentation = async (accessTokens, vcFileId
|
|
14
11
|
return null;
|
15
12
|
}
|
16
13
|
try {
|
17
|
-
const storage = new GoogleDriveStorage(accessTokens);
|
18
|
-
const engine = new CredentialEngine(accessTokens);
|
19
|
-
// Fetch Verifiable Credential (VC)
|
20
|
-
const verifiableCredential = await storage.retrieve(vcFileId);
|
21
|
-
if (!verifiableCredential) {
|
22
|
-
|
23
|
-
}
|
24
|
-
// Fetch VC comments (potential recommendations)
|
25
|
-
const verifiableCredentialComments = await storage.getFileComments(vcFileId);
|
26
|
-
let recommendations = [];
|
27
|
-
// Extract recommendations from comments if present
|
28
|
-
if (verifiableCredentialComments.length > 0) {
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
}
|
38
|
-
// Create Verifiable Presentation (VP) with the retrieved VC
|
39
|
-
const presentation = await engine.createPresentation([verifiableCredential.data, ...recommendations]); //! do not edit the array order!!
|
40
|
-
// Use the key pair to sign the presentation
|
41
|
-
const signedPresentation = await engine.signPresentation(presentation);
|
42
|
-
return {
|
14
|
+
// const storage = new GoogleDriveStorage(accessTokens);
|
15
|
+
// const engine = new CredentialEngine(accessTokens);
|
16
|
+
// // Fetch Verifiable Credential (VC)
|
17
|
+
// const verifiableCredential = await storage.retrieve(vcFileId);
|
18
|
+
// if (!verifiableCredential) {
|
19
|
+
// throw new Error('Verifiable Credential not found.');
|
20
|
+
// }
|
21
|
+
// // Fetch VC comments (potential recommendations)
|
22
|
+
// const verifiableCredentialComments = await storage.getFileComments(vcFileId);
|
23
|
+
// let recommendations: object[] = [];
|
24
|
+
// // Extract recommendations from comments if present
|
25
|
+
// if (verifiableCredentialComments.length > 0) {
|
26
|
+
// for (const comment of verifiableCredentialComments) {
|
27
|
+
// console.log('🚀 ~ createAndSignVerifiablePresentation ~ comment', comment);
|
28
|
+
// const recommendationFile = await storage.retrieve(extractGoogleDriveFileId(comment.content));
|
29
|
+
// console.log('🚀 ~ createAndSignVerifiablePresentation ~ recommendationFile', recommendationFile);
|
30
|
+
// if (recommendationFile) {
|
31
|
+
// recommendations.push(recommendationFile);
|
32
|
+
// }
|
33
|
+
// }
|
34
|
+
// }
|
35
|
+
// // Create Verifiable Presentation (VP) with the retrieved VC
|
36
|
+
// const presentation = await engine.createPresentation([verifiableCredential.data, ...recommendations]); //! do not edit the array order!!
|
37
|
+
// // Use the key pair to sign the presentation
|
38
|
+
// const signedPresentation = await engine.signPresentation(presentation);
|
39
|
+
// return {};
|
43
40
|
}
|
44
41
|
catch (error) {
|
45
42
|
console.error('Error during Verifiable Presentation creation and signing:', 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.
|
4
|
+
"version": "1.0.13",
|
5
5
|
"description": "Sign and store your verifiable credentials.",
|
6
6
|
"main": "dist/index.js",
|
7
7
|
"types": "dist/types/index.d.ts",
|
@@ -21,6 +21,7 @@
|
|
21
21
|
"@digitalbazaar/ed25519-verification-key-2020": "^4.1.0",
|
22
22
|
"@digitalbazaar/vc": "^6.3.0",
|
23
23
|
"ethers": "^6.13.2",
|
24
|
+
"ts-node": "^10.9.2",
|
24
25
|
"tsc": "^2.0.4",
|
25
26
|
"uuid": "^10.0.0"
|
26
27
|
},
|
@@ -1,10 +0,0 @@
|
|
1
|
-
import { GoogleDriveStorage } from '../models/GoogleDriveStorage.js';
|
2
|
-
/**
|
3
|
-
* Save data to Google Drive in the specified folder type.
|
4
|
-
* @param {object} data - The data to save.
|
5
|
-
* @param {'VC' | 'DID' | 'UnsignedVC'} type - The type of data being saved.
|
6
|
-
* @returns {Promise<object>} - The file object saved to Google Drive.
|
7
|
-
* @throws Will throw an error if the save operation fails.
|
8
|
-
*/
|
9
|
-
export declare function saveToGoogleDrive(storage: GoogleDriveStorage, data: any, type: 'VC' | 'DID' | 'SESSION' | 'RECOMMENDATION'): Promise<object>;
|
10
|
-
export declare function generateViewLink(fileId: string): string;
|
@@ -1,65 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Save data to Google Drive in the specified folder type.
|
3
|
-
* @param {object} data - The data to save.
|
4
|
-
* @param {'VC' | 'DID' | 'UnsignedVC'} type - The type of data being saved.
|
5
|
-
* @returns {Promise<object>} - The file object saved to Google Drive.
|
6
|
-
* @throws Will throw an error if the save operation fails.
|
7
|
-
*/
|
8
|
-
export async function saveToGoogleDrive(storage, data, type) {
|
9
|
-
try {
|
10
|
-
const timestamp = Date.now();
|
11
|
-
const fileData = {
|
12
|
-
fileName: `${type}-${timestamp}.json`,
|
13
|
-
mimeType: 'application/json',
|
14
|
-
body: JSON.stringify(data),
|
15
|
-
};
|
16
|
-
// Get all root folders
|
17
|
-
const rootFolders = await storage.findFolders();
|
18
|
-
console.log('Root folders:', rootFolders);
|
19
|
-
// Find or create the "Credentials" folder
|
20
|
-
let credentialsFolder = rootFolders.find((f) => f.name === 'Credentials');
|
21
|
-
let credentialsFolderId;
|
22
|
-
if (!credentialsFolder) {
|
23
|
-
credentialsFolderId = await storage.createFolder('Credentials');
|
24
|
-
console.log('Created Credentials folder with ID:', credentialsFolderId);
|
25
|
-
}
|
26
|
-
else {
|
27
|
-
credentialsFolderId = credentialsFolder.id;
|
28
|
-
console.log('Found Credentials folder with ID:', credentialsFolderId);
|
29
|
-
}
|
30
|
-
// Get subfolders within the "Credentials" folder
|
31
|
-
const subfolders = await storage.findFolders(credentialsFolderId);
|
32
|
-
console.log(`Subfolders in Credentials (ID: ${credentialsFolderId}):`, subfolders);
|
33
|
-
// Find or create the specific subfolder (DIDs or VCs)
|
34
|
-
let typeFolder = subfolders.find((f) => f.name === `${type}s`);
|
35
|
-
let typeFolderId;
|
36
|
-
if (!typeFolder) {
|
37
|
-
typeFolderId = await storage.createFolder(`${type}s`, credentialsFolderId);
|
38
|
-
console.log(`Created ${type}s folder with ID:`, typeFolderId);
|
39
|
-
}
|
40
|
-
else {
|
41
|
-
typeFolderId = typeFolder.id;
|
42
|
-
console.log(`Found ${type} files:`, await storage.findLastFile(typeFolderId));
|
43
|
-
console.log(`Found ${type}s folder with ID:`, typeFolderId);
|
44
|
-
}
|
45
|
-
// Save the file in the specific subfolder
|
46
|
-
const file = await storage.save(fileData, typeFolderId);
|
47
|
-
console.log(`File uploaded: ${file?.id} under ${type}s with ID ${typeFolderId} folder in Credentials folder`);
|
48
|
-
if (file && file.id) {
|
49
|
-
console.log('Sharing file with second user...');
|
50
|
-
await storage.addCommenterRoleToFile(file.id);
|
51
|
-
}
|
52
|
-
return file;
|
53
|
-
}
|
54
|
-
catch (error) {
|
55
|
-
console.error('Error saving to Google Drive:', error);
|
56
|
-
throw error;
|
57
|
-
}
|
58
|
-
}
|
59
|
-
export function generateViewLink(fileId) {
|
60
|
-
if (!fileId) {
|
61
|
-
throw new Error('File ID is required to generate a view link.');
|
62
|
-
}
|
63
|
-
// Construct the view URL based on the file ID
|
64
|
-
return `https://drive.google.com/file/d/${fileId}/view`;
|
65
|
-
}
|