@cooperation/vc-storage 1.0.27 → 1.0.29
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 +113 -0
- package/dist/models/GoogleDriveStorage.js +8 -0
- package/dist/models/ResumeVC.js +79 -7
- package/dist/tests/testEmailVC.js +29 -0
- package/dist/types/models/CredentialEngine.d.ts +9 -0
- package/dist/types/models/ResumeVC.d.ts +12 -0
- package/dist/types/tests/testEmailVC.d.ts +1 -0
- package/dist/types/utils/context.d.ts +3 -1
- package/dist/utils/context.js +4 -2
- package/package.json +1 -1
@@ -5,6 +5,9 @@ import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
import { extractKeyPairFromCredential, generateDIDSchema, generateUnsignedRecommendation, generateUnsignedVC, } from '../utils/credential.js';
|
6
6
|
import { customDocumentLoader } from '../utils/digitalbazaar.js';
|
7
7
|
import { saveToGoogleDrive } from '../utils/google.js';
|
8
|
+
function delay(ms) {
|
9
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
10
|
+
}
|
8
11
|
/**
|
9
12
|
* Class representing the Credential Engine.
|
10
13
|
* @class CredentialEngine
|
@@ -234,4 +237,114 @@ export class CredentialEngine {
|
|
234
237
|
throw error;
|
235
238
|
}
|
236
239
|
}
|
240
|
+
/**
|
241
|
+
* Generate and sign an email Verifiable Credential (VC)
|
242
|
+
* @param {string} email - The email address to create the VC for
|
243
|
+
* @returns {Promise<{signedVC: any, fileId: string}>} The signed VC and its Google Drive file ID
|
244
|
+
*/
|
245
|
+
async generateAndSignEmailVC(email) {
|
246
|
+
try {
|
247
|
+
// Try to find existing keys and DIDs
|
248
|
+
const existingKeys = await this.findKeysAndDIDs();
|
249
|
+
let keyPair;
|
250
|
+
let didDocument;
|
251
|
+
if (existingKeys) {
|
252
|
+
// Use existing keys and DID
|
253
|
+
keyPair = existingKeys.keyPair;
|
254
|
+
didDocument = existingKeys.didDocument;
|
255
|
+
}
|
256
|
+
else {
|
257
|
+
// Generate new key pair and DID if none exist
|
258
|
+
keyPair = await this.generateKeyPair();
|
259
|
+
const result = await this.createDID();
|
260
|
+
didDocument = result.didDocument;
|
261
|
+
}
|
262
|
+
// Generate unsigned email VC
|
263
|
+
const unsignedCredential = {
|
264
|
+
'@context': [
|
265
|
+
'https://www.w3.org/2018/credentials/v1',
|
266
|
+
{
|
267
|
+
'email': 'https://schema.org/email',
|
268
|
+
'EmailCredential': {
|
269
|
+
'@id': 'https://example.com/EmailCredential'
|
270
|
+
}
|
271
|
+
}
|
272
|
+
],
|
273
|
+
'id': `urn:uuid:${uuidv4()}`,
|
274
|
+
'type': ['VerifiableCredential', 'EmailCredential'],
|
275
|
+
'issuer': {
|
276
|
+
'id': didDocument.id
|
277
|
+
},
|
278
|
+
'issuanceDate': new Date().toISOString(),
|
279
|
+
'expirationDate': new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 1 year from now
|
280
|
+
'credentialSubject': {
|
281
|
+
'id': `did:email:${email}`,
|
282
|
+
'email': email
|
283
|
+
}
|
284
|
+
};
|
285
|
+
// Sign the VC
|
286
|
+
const suite = new Ed25519Signature2020({
|
287
|
+
key: keyPair,
|
288
|
+
verificationMethod: keyPair.id,
|
289
|
+
});
|
290
|
+
const signedVC = await dbVc.issue({
|
291
|
+
credential: unsignedCredential,
|
292
|
+
suite,
|
293
|
+
documentLoader: customDocumentLoader,
|
294
|
+
});
|
295
|
+
// Get root folders
|
296
|
+
const rootFolders = await this.storage.findFolders();
|
297
|
+
// Find or create Credentials folder
|
298
|
+
let credentialsFolder = rootFolders.find((f) => f.name === 'Credentials');
|
299
|
+
if (!credentialsFolder) {
|
300
|
+
credentialsFolder = await this.storage.createFolder({
|
301
|
+
folderName: 'Credentials',
|
302
|
+
parentFolderId: 'root'
|
303
|
+
});
|
304
|
+
// Wait and re-check to avoid duplicates
|
305
|
+
await delay(1500);
|
306
|
+
const refreshedFolders = await this.storage.findFolders();
|
307
|
+
const foundAgain = refreshedFolders.find((f) => f.name === 'Credentials');
|
308
|
+
if (foundAgain)
|
309
|
+
credentialsFolder = foundAgain;
|
310
|
+
}
|
311
|
+
// Find or create EMAIL_VC folder
|
312
|
+
const subfolders = await this.storage.findFolders(credentialsFolder.id);
|
313
|
+
let emailVcFolder = subfolders.find((f) => f.name === 'EMAIL_VC');
|
314
|
+
if (!emailVcFolder) {
|
315
|
+
emailVcFolder = await this.storage.createFolder({
|
316
|
+
folderName: 'EMAIL_VC',
|
317
|
+
parentFolderId: credentialsFolder.id
|
318
|
+
});
|
319
|
+
// Wait and re-check to avoid duplicates
|
320
|
+
await delay(1500);
|
321
|
+
const refreshedSubfolders = await this.storage.findFolders(credentialsFolder.id);
|
322
|
+
const foundAgain = refreshedSubfolders.find((f) => f.name === 'EMAIL_VC');
|
323
|
+
if (foundAgain)
|
324
|
+
emailVcFolder = foundAgain;
|
325
|
+
}
|
326
|
+
// Save the VC in the EMAIL_VC folder
|
327
|
+
const file = await this.storage.saveFile({
|
328
|
+
data: {
|
329
|
+
fileName: `${email}.vc`,
|
330
|
+
mimeType: 'application/json',
|
331
|
+
body: signedVC
|
332
|
+
},
|
333
|
+
folderId: emailVcFolder.id
|
334
|
+
});
|
335
|
+
// Only save key pair if it's new
|
336
|
+
if (!existingKeys) {
|
337
|
+
await saveToGoogleDrive({
|
338
|
+
storage: this.storage,
|
339
|
+
data: keyPair,
|
340
|
+
type: 'KEYPAIR',
|
341
|
+
});
|
342
|
+
}
|
343
|
+
return { signedVC, fileId: file.id };
|
344
|
+
}
|
345
|
+
catch (error) {
|
346
|
+
console.error('Error generating and signing email VC:', error);
|
347
|
+
throw error;
|
348
|
+
}
|
349
|
+
}
|
237
350
|
}
|
@@ -142,6 +142,14 @@ export class GoogleDriveStorage {
|
|
142
142
|
headers: {},
|
143
143
|
body: JSON.stringify({ role: 'reader', type: 'anyone' }),
|
144
144
|
});
|
145
|
+
// Invalidate cache for this parent folder
|
146
|
+
if (this.folderCache[parentFolderId]) {
|
147
|
+
delete this.folderCache[parentFolderId];
|
148
|
+
}
|
149
|
+
// Also clear 'root' cache if parent is root
|
150
|
+
if (parentFolderId === 'root' && this.folderCache['root']) {
|
151
|
+
delete this.folderCache['root'];
|
152
|
+
}
|
145
153
|
return folder;
|
146
154
|
}
|
147
155
|
async getMediaFolderId() {
|
package/dist/models/ResumeVC.js
CHANGED
@@ -7,18 +7,31 @@ import { generateDIDSchema } from '../utils/credential.js';
|
|
7
7
|
import { inlineResumeContext } from '../utils/context.js';
|
8
8
|
export class ResumeVC {
|
9
9
|
async sign({ formData, issuerDid, keyPair }) {
|
10
|
+
// First, generate the unsigned credential with the professional summary
|
10
11
|
const unsignedCredential = this.generateUnsignedCredential({ formData, issuerDid });
|
12
|
+
// Create the signature suite for signing
|
11
13
|
const suite = new Ed25519Signature2020({
|
12
|
-
key: new Ed25519VerificationKey2020(keyPair),
|
14
|
+
key: new Ed25519VerificationKey2020(keyPair),
|
13
15
|
verificationMethod: keyPair.id,
|
14
16
|
});
|
15
17
|
try {
|
16
|
-
|
18
|
+
// 1: Sign the professional summary VC first
|
19
|
+
const professionalSummaryVC = unsignedCredential.credentialSubject.professionalSummary;
|
20
|
+
const signedProfessionalSummaryVC = await dbVc.issue({
|
21
|
+
credential: professionalSummaryVC,
|
22
|
+
suite,
|
23
|
+
documentLoader: customDocumentLoader,
|
24
|
+
});
|
25
|
+
// Replace the unsigned professional summary with the signed one
|
26
|
+
unsignedCredential.credentialSubject.professionalSummary = signedProfessionalSummaryVC;
|
27
|
+
// 2: Now sign the entire resume VC (which includes the signed professional summary)
|
28
|
+
const signedResumeVC = await dbVc.issue({
|
17
29
|
credential: unsignedCredential,
|
18
30
|
suite,
|
19
31
|
documentLoader: customDocumentLoader,
|
20
32
|
});
|
21
|
-
console.log('Signed VC:',
|
33
|
+
console.log('Signed Resume VC:', signedResumeVC);
|
34
|
+
return signedResumeVC;
|
22
35
|
}
|
23
36
|
catch (error) {
|
24
37
|
console.error('Error signing VC:', error.message);
|
@@ -27,8 +40,34 @@ export class ResumeVC {
|
|
27
40
|
}
|
28
41
|
throw error;
|
29
42
|
}
|
30
|
-
return unsignedCredential;
|
31
43
|
}
|
44
|
+
generateProfessionalSummary = (aff) => {
|
45
|
+
let cleanNarrative = aff.narrative || '';
|
46
|
+
if (cleanNarrative.startsWith('<p>') && cleanNarrative.endsWith('</p>')) {
|
47
|
+
// Remove the opening and closing p tags
|
48
|
+
cleanNarrative = cleanNarrative.substring(3, cleanNarrative.length - 4);
|
49
|
+
}
|
50
|
+
// removing all <p> tags
|
51
|
+
cleanNarrative = cleanNarrative.replace(/<\/?p>/g, '');
|
52
|
+
// Replace <b> or <strong> tags with markdown bold syntax (**word**)
|
53
|
+
cleanNarrative = cleanNarrative.replace(/<b>(.*?)<\/b>/g, '**$1**');
|
54
|
+
cleanNarrative = cleanNarrative.replace(/<strong>(.*?)<\/strong>/g, '**$1**');
|
55
|
+
return {
|
56
|
+
'@context': [
|
57
|
+
'https://www.w3.org/2018/credentials/v1',
|
58
|
+
{
|
59
|
+
'@vocab': 'https://schema.hropenstandards.org/4.4/',
|
60
|
+
narrative: 'https://schema.org/narrative',
|
61
|
+
},
|
62
|
+
],
|
63
|
+
type: ['VerifiableCredential', 'NarrativeCredential'],
|
64
|
+
issuer: aff.issuer, // same DID as the issuer of the resume
|
65
|
+
issuanceDate: new Date().toISOString(),
|
66
|
+
credentialSubject: {
|
67
|
+
narrative: cleanNarrative,
|
68
|
+
},
|
69
|
+
};
|
70
|
+
};
|
32
71
|
generateUnsignedCredential({ formData, issuerDid }) {
|
33
72
|
const unsignedResumeVC = {
|
34
73
|
'@context': ['https://www.w3.org/2018/credentials/v1', inlineResumeContext['@context']],
|
@@ -62,9 +101,10 @@ export class ResumeVC {
|
|
62
101
|
},
|
63
102
|
},
|
64
103
|
},
|
65
|
-
|
66
|
-
|
67
|
-
|
104
|
+
professionalSummary: this.generateProfessionalSummary({
|
105
|
+
issuer: issuerDid,
|
106
|
+
narrative: formData.summary || '',
|
107
|
+
}),
|
68
108
|
employmentHistory: formData.experience.items.map((exp) => ({
|
69
109
|
id: exp.id ? `urn:uuid${exp.id}` : `urn:uuid:${uuidv4()}`, // Ensure each entry has an ID
|
70
110
|
organization: {
|
@@ -121,6 +161,38 @@ export class ResumeVC {
|
|
121
161
|
credentialLink: proj.credentialLink || null,
|
122
162
|
verifiedCredentials: proj.verifiedCredentials || [],
|
123
163
|
})),
|
164
|
+
professionalAffiliations: formData.professionalAffiliations.items.map((aff) => ({
|
165
|
+
id: aff.id ? `urn:uuid:${aff.id}` : `urn:uuid:${uuidv4()}`,
|
166
|
+
name: aff.name || '',
|
167
|
+
organization: aff.organization || '',
|
168
|
+
startDate: aff.startDate || '',
|
169
|
+
endDate: aff.endDate || '',
|
170
|
+
activeAffiliation: aff.activeAffiliation || false,
|
171
|
+
duration: aff.duration || '',
|
172
|
+
verificationStatus: aff.verificationStatus || 'unverified',
|
173
|
+
credentialLink: aff.credentialLink || '',
|
174
|
+
selectedCredentials: aff.selectedCredentials || [],
|
175
|
+
})),
|
176
|
+
volunteerWork: formData.volunteerWork.items.map((vol) => ({
|
177
|
+
id: vol.id ? `urn:uuid:${vol.id}` : `urn:uuid:${uuidv4()}`,
|
178
|
+
role: vol.role || '',
|
179
|
+
organization: vol.organization || '',
|
180
|
+
location: vol.location || '',
|
181
|
+
startDate: vol.startDate || '',
|
182
|
+
endDate: vol.endDate || '',
|
183
|
+
currentlyVolunteering: vol.currentlyVolunteering || false,
|
184
|
+
description: vol.description || '',
|
185
|
+
duration: vol.duration || '',
|
186
|
+
verificationStatus: vol.verificationStatus || 'unverified',
|
187
|
+
credentialLink: vol.credentialLink || '',
|
188
|
+
selectedCredentials: vol.selectedCredentials || [],
|
189
|
+
})),
|
190
|
+
hobbiesAndInterests: formData.hobbiesAndInterests || [],
|
191
|
+
languages: formData.languages.items.map((lang) => ({
|
192
|
+
id: lang.id ? `urn:uuid:${lang.id}` : `urn:uuid:${uuidv4()}`,
|
193
|
+
name: lang.name || '',
|
194
|
+
proficiency: lang.proficiency || '',
|
195
|
+
})),
|
124
196
|
},
|
125
197
|
};
|
126
198
|
console.log('🚀 ~ ResumeVC ~ generateUnsignedCredential ~ unsignedResumeVC:', JSON.stringify(unsignedResumeVC));
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { CredentialEngine } from '../models/CredentialEngine.js';
|
2
|
+
import { GoogleDriveStorage } from '../models/GoogleDriveStorage.js';
|
3
|
+
async function testEmailVC() {
|
4
|
+
try {
|
5
|
+
// Initialize storage and engine
|
6
|
+
const storage = new GoogleDriveStorage('YOUR_ACCESS_TOKEN'); // Replace with actual access token
|
7
|
+
const engine = new CredentialEngine(storage);
|
8
|
+
// Test email
|
9
|
+
const testEmail = 'test@example.com';
|
10
|
+
console.log('Starting email VC generation test...');
|
11
|
+
console.log('Test email:', testEmail);
|
12
|
+
// Generate and sign the email VC
|
13
|
+
const result = await engine.generateAndSignEmailVC(testEmail);
|
14
|
+
console.log('\nTest Results:');
|
15
|
+
console.log('-------------');
|
16
|
+
console.log('File ID:', result.fileId);
|
17
|
+
console.log('Signed VC:', JSON.stringify(result.signedVC, null, 2));
|
18
|
+
// Test retrieving the VC from storage
|
19
|
+
console.log('\nRetrieving VC from storage...');
|
20
|
+
const retrievedVC = await storage.retrieve(result.fileId);
|
21
|
+
console.log('Retrieved VC:', retrievedVC ? 'Success' : 'Failed');
|
22
|
+
console.log('\nTest completed successfully!');
|
23
|
+
}
|
24
|
+
catch (error) {
|
25
|
+
console.error('Test failed:', error);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
// Run the test
|
29
|
+
testEmailVC().catch(console.error);
|
@@ -78,5 +78,14 @@ export declare class CredentialEngine {
|
|
78
78
|
* @returns
|
79
79
|
*/
|
80
80
|
signPresentation(presentation: any): Promise<any>;
|
81
|
+
/**
|
82
|
+
* Generate and sign an email Verifiable Credential (VC)
|
83
|
+
* @param {string} email - The email address to create the VC for
|
84
|
+
* @returns {Promise<{signedVC: any, fileId: string}>} The signed VC and its Google Drive file ID
|
85
|
+
*/
|
86
|
+
generateAndSignEmailVC(email: string): Promise<{
|
87
|
+
signedVC: any;
|
88
|
+
fileId: string;
|
89
|
+
}>;
|
81
90
|
}
|
82
91
|
export {};
|
@@ -4,6 +4,18 @@ export declare class ResumeVC {
|
|
4
4
|
issuerDid: string;
|
5
5
|
keyPair: any;
|
6
6
|
}): Promise<any>;
|
7
|
+
generateProfessionalSummary: (aff: any) => {
|
8
|
+
'@context': (string | {
|
9
|
+
'@vocab': string;
|
10
|
+
narrative: string;
|
11
|
+
})[];
|
12
|
+
type: string[];
|
13
|
+
issuer: any;
|
14
|
+
issuanceDate: string;
|
15
|
+
credentialSubject: {
|
16
|
+
narrative: any;
|
17
|
+
};
|
18
|
+
};
|
7
19
|
generateUnsignedCredential({ formData, issuerDid }: {
|
8
20
|
formData: any;
|
9
21
|
issuerDid: string;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -4,7 +4,7 @@ export declare const inlineResumeContext: {
|
|
4
4
|
name: string;
|
5
5
|
formattedName: string;
|
6
6
|
primaryLanguage: string;
|
7
|
-
|
7
|
+
professionalSummary: string;
|
8
8
|
text: string;
|
9
9
|
contact: string;
|
10
10
|
email: string;
|
@@ -73,10 +73,12 @@ export declare const inlineResumeContext: {
|
|
73
73
|
};
|
74
74
|
organization: string;
|
75
75
|
role: string;
|
76
|
+
activeAffiliation: string;
|
76
77
|
volunteerWork: {
|
77
78
|
'@id': string;
|
78
79
|
'@container': string;
|
79
80
|
};
|
81
|
+
currentlyVolunteering: string;
|
80
82
|
hobbiesAndInterests: {
|
81
83
|
'@id': string;
|
82
84
|
'@container': string;
|
package/dist/utils/context.js
CHANGED
@@ -6,7 +6,7 @@ export const inlineResumeContext = {
|
|
6
6
|
formattedName: 'https://schema.org/formattedName',
|
7
7
|
primaryLanguage: 'https://schema.org/primaryLanguage',
|
8
8
|
// Narrative
|
9
|
-
|
9
|
+
professionalSummary: 'https://schema.org/professionalSummary',
|
10
10
|
text: 'https://schema.org/text',
|
11
11
|
// Contact Information
|
12
12
|
contact: 'https://schema.org/ContactPoint',
|
@@ -83,11 +83,13 @@ export const inlineResumeContext = {
|
|
83
83
|
},
|
84
84
|
organization: 'https://schema.org/memberOf',
|
85
85
|
role: 'https://schema.org/jobTitle',
|
86
|
+
activeAffiliation: 'https://schema.org/Boolean',
|
86
87
|
// Volunteer Work
|
87
88
|
volunteerWork: {
|
88
89
|
'@id': 'https://schema.org/VolunteerRole',
|
89
90
|
'@container': '@list',
|
90
91
|
},
|
92
|
+
currentlyVolunteering: 'https://schema.org/Boolean',
|
91
93
|
// Hobbies and Interests
|
92
94
|
hobbiesAndInterests: {
|
93
95
|
'@id': 'https://schema.org/knowsAbout',
|
@@ -114,7 +116,7 @@ export const inlineResumeContext = {
|
|
114
116
|
// Issuance Information
|
115
117
|
issuanceDate: 'https://schema.org/issuanceDate',
|
116
118
|
credentialSubject: 'https://schema.org/credentialSubject',
|
117
|
-
person: 'https://schema.org/Person',
|
119
|
+
person: 'https://schema.org/Person',
|
118
120
|
Resume: 'https://schema.hropenstandards.org/4.4#Resume',
|
119
121
|
},
|
120
122
|
};
|