@cooperation/vc-storage 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
-
}
|