@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.
@@ -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() {
@@ -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), // Ensure proper initialization
14
+ key: new Ed25519VerificationKey2020(keyPair),
13
15
  verificationMethod: keyPair.id,
14
16
  });
15
17
  try {
16
- const signedVC = await dbVc.issue({
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:', signedVC);
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
- narrative: {
66
- text: formData.summary || '',
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
- narrative: string;
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;
@@ -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
- narrative: 'https://schema.org/narrative',
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', // Map to Person schema
119
+ person: 'https://schema.org/Person',
118
120
  Resume: 'https://schema.hropenstandards.org/4.4#Resume',
119
121
  },
120
122
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cooperation/vc-storage",
3
3
  "type": "module",
4
- "version": "1.0.27",
4
+ "version": "1.0.29",
5
5
  "description": "Sign and store your verifiable credentials.",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/types/index.d.ts",