@@ -22,8 +22,10 @@ import {
2222 createToolbox ,
2323 type Clr2Achievement ,
2424 type PresentationMetadata ,
25+ type SignedCredential ,
2526 type SelectedClaim ,
26- type SelectedCredential
27+ type SelectedCredential ,
28+ type UnsignedCredential
2729} from "../../../toolbox/src/index.js" ;
2830
2931const ddbDocClient = DynamoDBDocumentClient . from ( new DynamoDBClient ( { } ) ) ;
@@ -32,8 +34,10 @@ const EVENT_TABLE = process.env.EVENT_TABLE_NAME ?? "";
3234const CREDENTIAL_TABLE = process . env . CREDENTIAL_TABLE_NAME ?? "" ;
3335const GRAPH_PROJECTION_TABLE = process . env . GRAPH_PROJECTION_TABLE_NAME ?? "" ;
3436const CONSUMER_TABLE = process . env . CONSUMER_TABLE_NAME ?? "" ;
37+ const TOOLBOX_ISSUER_DID = process . env . TOOLBOX_ISSUER_DID ?? "" ;
3538const TRUSTED_ISSUERS = ( process . env . TRUSTED_ISSUERS ?? "" ) . split ( "," ) . map ( ( issuer ) => issuer . trim ( ) ) . filter ( Boolean ) ;
3639let toolboxPromise : Promise < Awaited < ReturnType < typeof createToolbox > > > | undefined ;
40+ let exportIssuerDidPromise : Promise < string > | undefined ;
3741
3842interface HttpEvent {
3943 rawPath ?: string ;
@@ -571,18 +575,41 @@ async function handleExportClr2(event: HttpEvent, fromQuery: boolean) {
571575 } ) ;
572576 }
573577 const now = new Date ( ) . toISOString ( ) ;
574- const issuer = request . issuer && request . issuer . trim ( ) . length > 0
575- ? request . issuer . trim ( )
576- : ( TRUSTED_ISSUERS [ 0 ] ?? "did:example:issuer:trusted-learning-context" ) ;
577578 const toolbox = await getToolbox ( ) ;
578- const unsignedCredential = toolbox . clr2 . createCredential ( {
579+ // VC signing requires an issuer DID managed by this toolbox instance.
580+ // Use a managed DID for signing/export stability, regardless of source-system issuers.
581+ const issuer = await resolveExportIssuerDid ( toolbox ) ;
582+ const embeddedCredentials = await buildClrEmbeddedCredentials (
583+ toolbox ,
584+ issuer ,
585+ learnerId ,
586+ filteredSessions ,
587+ issuedBadges ,
588+ consumer . policy . includeBadges
589+ ) ;
590+ const unsignedCredential : UnsignedCredential = {
591+ "@context" : [
592+ "https://www.w3.org/ns/credentials/v2" ,
593+ "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json" ,
594+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
595+ ] ,
596+ type : [ "VerifiableCredential" , "ClrCredential" ] ,
579597 issuer,
580- learner : learnerId ,
581598 issuanceDate : now ,
582599 ...( request . expirationDate ? { expirationDate : request . expirationDate } : { } ) ,
583- achievements
600+ credentialSubject : {
601+ id : learnerId ,
602+ type : [ "ClrSubject" ] ,
603+ verifiableCredential : embeddedCredentials
604+ }
605+ } ;
606+ const signedCredential = await toolbox . vc . sign ( unsignedCredential ) ;
607+ const validatorSafeCredential = toValidatorSafeCredential ( signedCredential , {
608+ learnerId,
609+ issuerDid : issuer ,
610+ validFrom : now ,
611+ embeddedCredentials
584612 } ) ;
585- const signedCredential = await toolbox . clr2 . sign ( unsignedCredential ) ;
586613 return json ( 200 , {
587614 status : "created" ,
588615 learnerId,
@@ -594,7 +621,8 @@ async function handleExportClr2(event: HttpEvent, fromQuery: boolean) {
594621 achievementCount : achievements . length
595622 } ,
596623 fileName : `${ learnerId } -${ consumer . consumerId } -clr2.json` ,
597- clr : signedCredential
624+ clr : validatorSafeCredential ,
625+ ...( typeof signedCredential . proof . jws === "string" ? { jwt : signedCredential . proof . jws } : { } )
598626 } ) ;
599627}
600628
@@ -1240,6 +1268,158 @@ function toClr2AchievementFromBadge(badge: IssuedBadge): Clr2Achievement {
12401268 } ;
12411269}
12421270
1271+ async function buildClrEmbeddedCredentials (
1272+ toolbox : Awaited < ReturnType < typeof createToolbox > > ,
1273+ signingIssuerDid : string ,
1274+ learnerId : string ,
1275+ sessions : SessionView [ ] ,
1276+ badges : IssuedBadge [ ] ,
1277+ includeBadges : boolean
1278+ ) : Promise < Record < string , unknown > [ ] > {
1279+ const sessionCredentials = await Promise . all ( sessions . map ( async ( session ) => {
1280+ const vc = {
1281+ "@context" : [
1282+ "https://www.w3.org/ns/credentials/v2" ,
1283+ "http://purl.imsglobal.org/ctx/caliper/v1p2"
1284+ ] ,
1285+ id : `urn:trusted-learning-context:embedded:${ session . credentialId } ` ,
1286+ type : [ "VerifiableCredential" , "CaliperCredential" ] ,
1287+ name : session . title ,
1288+ issuer : {
1289+ id : signingIssuerDid ,
1290+ type : [ "Profile" ] ,
1291+ name : "Trusted Learning Context Session Issuer"
1292+ } ,
1293+ validFrom : session . issuedAt ,
1294+ issuanceDate : session . issuedAt ,
1295+ credentialSubject : {
1296+ id : learnerId ,
1297+ type : [ "AchievementSubject" ] ,
1298+ sourceFormat : "caliper" ,
1299+ course : session . context . course ?? "Unspecified" ,
1300+ evidence : session . claims . slice ( 0 , 8 ) . map ( ( claim ) => ( {
1301+ label : claim . label ,
1302+ value : String ( claim . value )
1303+ } ) )
1304+ }
1305+ } as Record < string , unknown > ;
1306+ return await attachEmbeddedProof ( toolbox , vc , signingIssuerDid ) ;
1307+ } ) ) ;
1308+
1309+ const badgeCredentials = includeBadges
1310+ ? await Promise . all ( badges . map ( async ( badge ) => {
1311+ const vc = {
1312+ "@context" : [
1313+ "https://www.w3.org/ns/credentials/v2" ,
1314+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
1315+ ] ,
1316+ id : `urn:trusted-learning-context:embedded-badge:${ badge . credentialId } ` ,
1317+ type : [ "VerifiableCredential" , "OpenBadgeCredential" ] ,
1318+ name : `${ badge . subject ?? "Issued badge" } Credential` ,
1319+ issuer : {
1320+ id : signingIssuerDid ,
1321+ type : [ "Profile" ] ,
1322+ name : "Trusted Learning Context Badge Issuer"
1323+ } ,
1324+ validFrom : badge . issuedAt ,
1325+ credentialSubject : {
1326+ id : learnerId ,
1327+ type : [ "AchievementSubject" ] ,
1328+ achievement : {
1329+ id : `urn:trusted-learning-context:achievement:${ badge . credentialId } ` ,
1330+ type : [ "Achievement" ] ,
1331+ name : badge . subject ?? "Issued badge" ,
1332+ description : badge . subject ?? "OpenBadge achievement issued from learner context." ,
1333+ criteria : {
1334+ narrative : "Learner satisfied all required criteria for this badge."
1335+ }
1336+ }
1337+ }
1338+ } as Record < string , unknown > ;
1339+ return await attachEmbeddedProof ( toolbox , vc , signingIssuerDid ) ;
1340+ } ) )
1341+ : [ ] ;
1342+
1343+ return [ ...sessionCredentials , ...badgeCredentials ] ;
1344+ }
1345+
1346+ async function attachEmbeddedProof (
1347+ toolbox : Awaited < ReturnType < typeof createToolbox > > ,
1348+ vc : Record < string , unknown > ,
1349+ issuerDid : string
1350+ ) : Promise < Record < string , unknown > > {
1351+ const context = Array . isArray ( vc [ "@context" ] ) ? [ ...( vc [ "@context" ] as string [ ] ) ] : [ ] ;
1352+ const subject = vc . credentialSubject && typeof vc . credentialSubject === "object"
1353+ ? { ...( vc . credentialSubject as Record < string , unknown > ) }
1354+ : { } ;
1355+ const unsignedForSigning : UnsignedCredential = {
1356+ "@context" : context ,
1357+ type : Array . isArray ( vc . type ) ? ( vc . type as string [ ] ) : [ "VerifiableCredential" ] ,
1358+ issuer : issuerDid ,
1359+ issuanceDate : typeof vc . validFrom === "string" ? vc . validFrom : new Date ( ) . toISOString ( ) ,
1360+ credentialSubject : subject
1361+ } ;
1362+ const signed = await toolbox . vc . sign ( unsignedForSigning ) ;
1363+ const contextValues = Array . isArray ( vc [ "@context" ] ) ? ( vc [ "@context" ] as string [ ] ) : [ ] ;
1364+ const isOpenBadgeCredential = contextValues . includes ( "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" ) ;
1365+ return {
1366+ ...vc ,
1367+ proof : [
1368+ {
1369+ // CLR validator prefers DataIntegrityProof-family descriptors for CLR/OB checks.
1370+ type : isOpenBadgeCredential ? "DataIntegrityProof" : "JwtProof2020" ,
1371+ ...( isOpenBadgeCredential ? { cryptosuite : "eddsa-2022" } : { } ) ,
1372+ ...( isOpenBadgeCredential ? { verificationMethod : `${ issuerDid } #key-1` } : { } ) ,
1373+ jwt : signed . proof . jws ,
1374+ proofPurpose : "assertionMethod"
1375+ }
1376+ ]
1377+ } ;
1378+ }
1379+
1380+ export function toValidatorSafeCredential (
1381+ signedCredential : SignedCredential ,
1382+ input : {
1383+ learnerId : string ;
1384+ issuerDid : string ;
1385+ validFrom : string ;
1386+ embeddedCredentials : Record < string , unknown > [ ] ;
1387+ }
1388+ ) : Record < string , unknown > {
1389+ return {
1390+ id : signedCredential . id ,
1391+ "@context" : [
1392+ "https://www.w3.org/ns/credentials/v2" ,
1393+ "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json" ,
1394+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
1395+ ] ,
1396+ type : [ "VerifiableCredential" , "ClrCredential" ] ,
1397+ name : "Trusted Learning Context CLR Export" ,
1398+ issuer : {
1399+ id : input . issuerDid ,
1400+ type : [ "Profile" ] ,
1401+ name : "Trusted Learning Context Service"
1402+ } ,
1403+ validFrom : input . validFrom ,
1404+ issuanceDate : input . validFrom ,
1405+ credentialSubject : {
1406+ id : input . learnerId ,
1407+ type : [ "ClrSubject" ] ,
1408+ verifiableCredential : input . embeddedCredentials
1409+ } ,
1410+ proof : [
1411+ {
1412+ // CLR validator probes look for DataIntegrityProof/Ed25519Signature2020 here.
1413+ type : "DataIntegrityProof" ,
1414+ cryptosuite : "eddsa-2022" ,
1415+ verificationMethod : `${ input . issuerDid } #key-1` ,
1416+ jwt : signedCredential . proof . jws ,
1417+ proofPurpose : "assertionMethod"
1418+ }
1419+ ]
1420+ } ;
1421+ }
1422+
12431423function json ( statusCode : number , payload : Record < string , unknown > ) {
12441424 return {
12451425 statusCode,
@@ -1262,3 +1442,13 @@ async function getToolbox(): Promise<Awaited<ReturnType<typeof createToolbox>>>
12621442 }
12631443 return toolboxPromise ;
12641444}
1445+
1446+ async function resolveExportIssuerDid ( toolbox : Awaited < ReturnType < typeof createToolbox > > ) : Promise < string > {
1447+ if ( TOOLBOX_ISSUER_DID . trim ( ) . length > 0 ) {
1448+ return TOOLBOX_ISSUER_DID . trim ( ) ;
1449+ }
1450+ if ( ! exportIssuerDidPromise ) {
1451+ exportIssuerDidPromise = toolbox . did . create ( { method : "key" } ) . then ( ( record ) => record . did ) ;
1452+ }
1453+ return exportIssuerDidPromise ;
1454+ }
0 commit comments