@learncard/linked-claims-plugin 0.2.35 → 0.2.36
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/README.md +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/linked-claims-plugin.cjs.development.cjs +6 -15
- package/dist/linked-claims-plugin.cjs.development.cjs.map +2 -2
- package/dist/linked-claims-plugin.cjs.production.min.cjs +1 -1
- package/dist/linked-claims-plugin.cjs.production.min.cjs.map +3 -3
- package/dist/linked-claims-plugin.esm.js +6 -15
- package/dist/linked-claims-plugin.esm.js.map +2 -2
- package/dist/test/store-endorsement.test.d.ts +2 -0
- package/dist/test/store-endorsement.test.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +8 -18
- package/src/test/linked-claims.test.ts +243 -190
- package/src/test/store-endorsement.test.ts +65 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# LinkedClaims Plugin
|
|
2
2
|
|
|
3
|
-
Create, verify, store, and retrieve endorsement credentials using Open Badges v3 EndorsementCredential, linked to original
|
|
3
|
+
Create, verify, store, and retrieve endorsement credentials using Open Badges v3 EndorsementCredential, linked to each original credential by its unique top-level id.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -20,10 +20,13 @@ const lc = await LearnCard.fromSomewhere().then(lc => lc.addPlugin(getLinkedClai
|
|
|
20
20
|
const endorsement = await lc.invoke.endorseCredential(originalVc, {
|
|
21
21
|
// OBv3 fields
|
|
22
22
|
endorsementComment: 'I endorse this credential.',
|
|
23
|
-
name:
|
|
23
|
+
name: 'Endorsement of First Aid',
|
|
24
24
|
description: 'Peer endorsement',
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
// The original credential must have a unique top-level id. A holder DID in
|
|
28
|
+
// credentialSubject.id does not identify a specific credential.
|
|
29
|
+
|
|
27
30
|
const verified = await lc.invoke.verifyEndorsement(endorsement);
|
|
28
31
|
|
|
29
32
|
const { uri } = await lc.invoke.storeEndorsement(endorsement);
|
|
@@ -44,6 +47,6 @@ Uses VC v2 and OBv3 contexts:
|
|
|
44
47
|
|
|
45
48
|
### Endorsement Fields (details input)
|
|
46
49
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
+
- `endorsementComment?: string`
|
|
51
|
+
- `name?: string`
|
|
52
|
+
- `description?: string`
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kCAAkC,EAAE,MAAM,SAAS,CAAC;AAEtF,cAAc,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kCAAkC,EAAE,MAAM,SAAS,CAAC;AAEtF,cAAc,SAAS,CAAC;AAwBxB,eAAO,MAAM,qBAAqB,GAC9B,WAAW,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,EAAE,kCAAkC,CAAC,KACjG,kBA2ID,CAAC"}
|
|
@@ -25,11 +25,6 @@ __export(index_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
27
|
var ENDORSEMENT_CONTEXT = "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json";
|
|
28
|
-
var getSubjectIds = /* @__PURE__ */ __name((credential) => {
|
|
29
|
-
const subject = credential.credentialSubject;
|
|
30
|
-
if (Array.isArray(subject)) return subject.map((s) => s?.id).filter(Boolean);
|
|
31
|
-
return subject?.id ? [subject.id] : [];
|
|
32
|
-
}, "getSubjectIds");
|
|
33
28
|
var pickIndexProvider = /* @__PURE__ */ __name((learnCard, preferred) => {
|
|
34
29
|
const providers = Object.keys(learnCard.index || {}).filter(
|
|
35
30
|
(k) => !["providers", "all"].includes(k)
|
|
@@ -53,11 +48,8 @@ var getLinkedClaimsPlugin = /* @__PURE__ */ __name((learnCard) => ({
|
|
|
53
48
|
methods: {
|
|
54
49
|
endorseCredential: /* @__PURE__ */ __name(async (_learnCard, original, details, opts) => {
|
|
55
50
|
const issuer = learnCard.id.did();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const targetId = originalId || subjectIds[0];
|
|
59
|
-
if (!targetId)
|
|
60
|
-
throw new Error("Original credential must have either id or credentialSubject.id");
|
|
51
|
+
const targetId = original.id;
|
|
52
|
+
if (!targetId) throw new Error("Original credential must have an id");
|
|
61
53
|
const context = [
|
|
62
54
|
"https://www.w3.org/ns/credentials/v2",
|
|
63
55
|
ENDORSEMENT_CONTEXT
|
|
@@ -133,8 +125,8 @@ var getLinkedClaimsPlugin = /* @__PURE__ */ __name((learnCard) => ({
|
|
|
133
125
|
originalCredentialId: csObj?.id,
|
|
134
126
|
issuedOn: endorsement.validFrom || endorsement.issuanceDate,
|
|
135
127
|
category: "Endorsement",
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
title: endorsement.name,
|
|
129
|
+
credentialId: options?.credentialId ?? csObj?.id,
|
|
138
130
|
sharedUri: options?.sharedUri,
|
|
139
131
|
// original credential shared uri
|
|
140
132
|
relationship: options?.relationship,
|
|
@@ -147,9 +139,8 @@ var getLinkedClaimsPlugin = /* @__PURE__ */ __name((learnCard) => ({
|
|
|
147
139
|
getEndorsements: /* @__PURE__ */ __name(async (_learnCard, original, options) => {
|
|
148
140
|
const indexName = pickIndexProvider(learnCard, options?.indexName);
|
|
149
141
|
if (!indexName) return [];
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const query = originalId ? { originalCredentialId: originalId } : { endorsedId: subjectIds[0] };
|
|
142
|
+
if (!original.id) return [];
|
|
143
|
+
const query = { originalCredentialId: original.id };
|
|
153
144
|
const records = await learnCard.index[indexName].get(query);
|
|
154
145
|
const results = [];
|
|
155
146
|
for (const rec of records) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { LearnCard } from '@learncard/core';\nimport type { VC, UnsignedVC } from '@learncard/types';\nimport type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';\n\nexport * from './types';\n\nconst ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';\n\nconst
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,IAAM,sBAAsB;AAE5B,IAAM,
|
|
4
|
+
"sourcesContent": ["import type { LearnCard } from '@learncard/core';\nimport type { VC, UnsignedVC } from '@learncard/types';\nimport type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';\n\nexport * from './types';\n\nconst ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';\n\nconst pickIndexProvider = (learnCard: any, preferred?: string): string | undefined => {\n const providers = Object.keys(learnCard.index || {}).filter(\n k => !['providers', 'all'].includes(k)\n );\n if (!providers.length) return undefined;\n if (preferred && providers.includes(preferred)) return preferred;\n if (providers.includes('LearnCloud')) return 'LearnCloud';\n return providers[0];\n};\n\nconst pickStoreProvider = (learnCard: any, preferred?: string): string | undefined => {\n const providers = Object.keys(learnCard.store || {}).filter(k => k !== 'providers');\n if (!providers.length) return undefined;\n if (preferred && providers.includes(preferred)) return preferred;\n return providers[0];\n};\n\nconst ensureArray = <T>(arr: T | T[] | undefined): T[] =>\n Array.isArray(arr) ? arr : arr ? [arr] : [];\n\nexport const getLinkedClaimsPlugin = (\n learnCard: LearnCard<any, 'id' | 'store' | 'index' | 'read', LinkedClaimsPluginDependentMethods>\n): LinkedClaimsPlugin => ({\n name: 'LinkedClaims',\n displayName: 'LinkedClaims',\n description: 'Create, verify, store, and retrieve endorsement credentials linked to originals.',\n methods: {\n endorseCredential: async (_learnCard, original, details, opts) => {\n const issuer = learnCard.id.did();\n\n const targetId = original.id;\n if (!targetId) throw new Error('Original credential must have an id');\n\n const context: (string | Record<string, any>)[] = [\n 'https://www.w3.org/ns/credentials/v2',\n ENDORSEMENT_CONTEXT,\n ];\n\n const types: [string, ...string[]] = ['VerifiableCredential', 'EndorsementCredential'];\n\n const unsigned: UnsignedVC = {\n '@context': context as any,\n type: types,\n issuer,\n validFrom: new Date().toISOString(),\n credentialSubject: {\n type: ['EndorsementSubject'],\n id: targetId,\n ...(details.endorsementComment\n ? { endorsementComment: details.endorsementComment }\n : {}),\n },\n ...(details.name ? { name: details.name } : { name: `Endorsement of ${targetId}` }),\n ...(details.description ? { description: details.description } : {}),\n ...(details.evidence && { evidence: details.evidence as any }),\n };\n\n return learnCard.invoke.issueCredential(unsigned);\n },\n\n verifyEndorsement: async (_learnCard, endorsement, options) => {\n const check = await learnCard.invoke.verifyCredential(endorsement, options);\n\n const ctx = ensureArray((endorsement as any)['@context']);\n const types = ensureArray((endorsement as any)['type']);\n\n if (\n !ctx.some((c: string) => c === 'https://www.w3.org/ns/credentials/v2') ||\n !ctx.some((c: string) => c === ENDORSEMENT_CONTEXT)\n ) {\n check.errors.push('linked-claims error: missing VCv2 or OBv3 context');\n }\n if (!types.includes('EndorsementCredential')) {\n check.errors.push('linked-claims error: missing EndorsementCredential type');\n }\n\n const cs: any = (endorsement as any).credentialSubject;\n const csObj = Array.isArray(cs) ? cs[0] : cs;\n const csTypes = ensureArray<string>(csObj?.type);\n if (!csTypes.includes('EndorsementSubject')) {\n check.errors.push(\n 'linked-claims error: credentialSubject.type must include EndorsementSubject'\n );\n }\n if (!csObj?.id) {\n check.errors.push('linked-claims error: credentialSubject.id missing');\n }\n\n if (!check.errors.length) check.checks.push('linked-claims');\n return check;\n },\n\n storeEndorsement: async (_learnCard, endorsement, options) => {\n const storeName = pickStoreProvider(learnCard, options?.storeName);\n if (!storeName) throw new Error('No store plane provider available');\n if (\n !learnCard.store[storeName].uploadEncrypted ||\n typeof learnCard.store[storeName].uploadEncrypted !== 'function'\n ) {\n throw new Error(\n `Store provider '${storeName}' does not support uploadEncrypted method`\n );\n }\n const uri: string = await (learnCard.store as any)[storeName].uploadEncrypted(\n endorsement\n );\n\n const indexName = pickIndexProvider(learnCard, options?.indexName);\n let indexed = false;\n // prettier-ignore\n let id =\n (endorsement as any).id ||\n `urn:uuid:${globalThis.crypto?.randomUUID?.() ||\n (() => {\n throw new Error(\n 'Secure random UUID generation is not available. Please use an environment that supports crypto.randomUUID().'\n );\n })()\n }`;\n\n if (indexName) {\n const cs: any = (endorsement as any).credentialSubject;\n const csObj = Array.isArray(cs) ? cs[0] : cs;\n const record = {\n id,\n uri,\n type: ensureArray((endorsement as any).type),\n endorsedId: csObj?.id,\n subjectId: csObj?.id,\n originalCredentialId: csObj?.id,\n issuedOn: (endorsement as any).validFrom || (endorsement as any).issuanceDate,\n category: 'Endorsement',\n title: endorsement.name,\n credentialId: options?.credentialId ?? csObj?.id,\n sharedUri: options?.sharedUri, // original credential shared uri\n relationship: options?.relationship,\n visibility: options?.visibility ?? 'public',\n };\n indexed = await (learnCard.index as any)[indexName].add(record);\n }\n\n return { uri, indexed, id, indexName, storeName };\n },\n\n getEndorsements: async (_learnCard, original, options) => {\n const indexName = pickIndexProvider(learnCard, options?.indexName);\n if (!indexName) return [];\n\n if (!original.id) return [];\n\n const query = { originalCredentialId: original.id };\n const records = await (learnCard.index as any)[indexName].get(query);\n\n const results: VC[] = [];\n for (const rec of records) {\n const vc = await learnCard.read.get(rec.uri);\n if (vc) results.push(vc as VC);\n }\n return results;\n },\n },\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var y=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var c=(e,i)=>y(e,"name",{value:i,configurable:!0});var E=(e,i)=>{for(var n in i)y(e,n,{get:i[n],enumerable:!0})},b=(e,i,n,r)=>{if(i&&typeof i=="object"||typeof i=="function")for(let t of h(i))!C.call(e,t)&&t!==n&&y(e,t,{get:()=>i[t],enumerable:!(r=v(i,t))||r.enumerable});return e};var w=e=>b(y({},"__esModule",{value:!0}),e);var j={};E(j,{getLinkedClaimsPlugin:()=>x});module.exports=w(j);var m="https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",g=c((e,i)=>{let n=Object.keys(e.index||{}).filter(r=>!["providers","all"].includes(r));if(n.length)return i&&n.includes(i)?i:n.includes("LearnCloud")?"LearnCloud":n[0]},"pickIndexProvider"),k=c((e,i)=>{let n=Object.keys(e.store||{}).filter(r=>r!=="providers");if(n.length)return i&&n.includes(i)?i:n[0]},"pickStoreProvider"),p=c(e=>Array.isArray(e)?e:e?[e]:[],"ensureArray"),x=c(e=>({name:"LinkedClaims",displayName:"LinkedClaims",description:"Create, verify, store, and retrieve endorsement credentials linked to originals.",methods:{endorseCredential:c(async(i,n,r,t)=>{let d=e.id.did(),s=n.id;if(!s)throw new Error("Original credential must have an id");let a={"@context":["https://www.w3.org/ns/credentials/v2",m],type:["VerifiableCredential","EndorsementCredential"],issuer:d,validFrom:new Date().toISOString(),credentialSubject:{type:["EndorsementSubject"],id:s,...r.endorsementComment?{endorsementComment:r.endorsementComment}:{}},...r.name?{name:r.name}:{name:`Endorsement of ${s}`},...r.description?{description:r.description}:{},...r.evidence&&{evidence:r.evidence}};return e.invoke.issueCredential(a)},"endorseCredential"),verifyEndorsement:c(async(i,n,r)=>{let t=await e.invoke.verifyCredential(n,r),d=p(n["@context"]),s=p(n.type);(!d.some(u=>u==="https://www.w3.org/ns/credentials/v2")||!d.some(u=>u===m))&&t.errors.push("linked-claims error: missing VCv2 or OBv3 context"),s.includes("EndorsementCredential")||t.errors.push("linked-claims error: missing EndorsementCredential type");let o=n.credentialSubject,l=Array.isArray(o)?o[0]:o;return p(l?.type).includes("EndorsementSubject")||t.errors.push("linked-claims error: credentialSubject.type must include EndorsementSubject"),l?.id||t.errors.push("linked-claims error: credentialSubject.id missing"),t.errors.length||t.checks.push("linked-claims"),t},"verifyEndorsement"),storeEndorsement:c(async(i,n,r)=>{let t=k(e,r?.storeName);if(!t)throw new Error("No store plane provider available");if(!e.store[t].uploadEncrypted||typeof e.store[t].uploadEncrypted!="function")throw new Error(`Store provider '${t}' does not support uploadEncrypted method`);let d=await e.store[t].uploadEncrypted(n),s=g(e,r?.indexName),o=!1,l=n.id||`urn:uuid:${globalThis.crypto?.randomUUID?.()||(()=>{throw new Error("Secure random UUID generation is not available. Please use an environment that supports crypto.randomUUID().")})()}`;if(s){let a=n.credentialSubject,u=Array.isArray(a)?a[0]:a,f={id:l,uri:d,type:p(n.type),endorsedId:u?.id,subjectId:u?.id,originalCredentialId:u?.id,issuedOn:n.validFrom||n.issuanceDate,category:"Endorsement",title:n.name,credentialId:r?.credentialId??u?.id,sharedUri:r?.sharedUri,relationship:r?.relationship,visibility:r?.visibility??"public"};o=await e.index[s].add(f)}return{uri:d,indexed:o,id:l,indexName:s,storeName:t}},"storeEndorsement"),getEndorsements:c(async(i,n,r)=>{let t=g(e,r?.indexName);if(!t)return[];if(!n.id)return[];let d={originalCredentialId:n.id},s=await e.index[t].get(d),o=[];for(let l of s){let a=await e.read.get(l.uri);a&&o.push(a)}return o},"getEndorsements")}}),"getLinkedClaimsPlugin");
|
|
2
2
|
//# sourceMappingURL=linked-claims-plugin.cjs.production.min.cjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { LearnCard } from '@learncard/core';\nimport type { VC, UnsignedVC } from '@learncard/types';\nimport type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';\n\nexport * from './types';\n\nconst ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';\n\nconst
|
|
5
|
-
"mappings": "4dAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,2BAAAE,IAAA,eAAAC,EAAAH,GAMA,IAAMI,EAAsB,6DAEtBC,
|
|
6
|
-
"names": ["index_exports", "__export", "getLinkedClaimsPlugin", "__toCommonJS", "ENDORSEMENT_CONTEXT", "
|
|
4
|
+
"sourcesContent": ["import type { LearnCard } from '@learncard/core';\nimport type { VC, UnsignedVC } from '@learncard/types';\nimport type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';\n\nexport * from './types';\n\nconst ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';\n\nconst pickIndexProvider = (learnCard: any, preferred?: string): string | undefined => {\n const providers = Object.keys(learnCard.index || {}).filter(\n k => !['providers', 'all'].includes(k)\n );\n if (!providers.length) return undefined;\n if (preferred && providers.includes(preferred)) return preferred;\n if (providers.includes('LearnCloud')) return 'LearnCloud';\n return providers[0];\n};\n\nconst pickStoreProvider = (learnCard: any, preferred?: string): string | undefined => {\n const providers = Object.keys(learnCard.store || {}).filter(k => k !== 'providers');\n if (!providers.length) return undefined;\n if (preferred && providers.includes(preferred)) return preferred;\n return providers[0];\n};\n\nconst ensureArray = <T>(arr: T | T[] | undefined): T[] =>\n Array.isArray(arr) ? arr : arr ? [arr] : [];\n\nexport const getLinkedClaimsPlugin = (\n learnCard: LearnCard<any, 'id' | 'store' | 'index' | 'read', LinkedClaimsPluginDependentMethods>\n): LinkedClaimsPlugin => ({\n name: 'LinkedClaims',\n displayName: 'LinkedClaims',\n description: 'Create, verify, store, and retrieve endorsement credentials linked to originals.',\n methods: {\n endorseCredential: async (_learnCard, original, details, opts) => {\n const issuer = learnCard.id.did();\n\n const targetId = original.id;\n if (!targetId) throw new Error('Original credential must have an id');\n\n const context: (string | Record<string, any>)[] = [\n 'https://www.w3.org/ns/credentials/v2',\n ENDORSEMENT_CONTEXT,\n ];\n\n const types: [string, ...string[]] = ['VerifiableCredential', 'EndorsementCredential'];\n\n const unsigned: UnsignedVC = {\n '@context': context as any,\n type: types,\n issuer,\n validFrom: new Date().toISOString(),\n credentialSubject: {\n type: ['EndorsementSubject'],\n id: targetId,\n ...(details.endorsementComment\n ? { endorsementComment: details.endorsementComment }\n : {}),\n },\n ...(details.name ? { name: details.name } : { name: `Endorsement of ${targetId}` }),\n ...(details.description ? { description: details.description } : {}),\n ...(details.evidence && { evidence: details.evidence as any }),\n };\n\n return learnCard.invoke.issueCredential(unsigned);\n },\n\n verifyEndorsement: async (_learnCard, endorsement, options) => {\n const check = await learnCard.invoke.verifyCredential(endorsement, options);\n\n const ctx = ensureArray((endorsement as any)['@context']);\n const types = ensureArray((endorsement as any)['type']);\n\n if (\n !ctx.some((c: string) => c === 'https://www.w3.org/ns/credentials/v2') ||\n !ctx.some((c: string) => c === ENDORSEMENT_CONTEXT)\n ) {\n check.errors.push('linked-claims error: missing VCv2 or OBv3 context');\n }\n if (!types.includes('EndorsementCredential')) {\n check.errors.push('linked-claims error: missing EndorsementCredential type');\n }\n\n const cs: any = (endorsement as any).credentialSubject;\n const csObj = Array.isArray(cs) ? cs[0] : cs;\n const csTypes = ensureArray<string>(csObj?.type);\n if (!csTypes.includes('EndorsementSubject')) {\n check.errors.push(\n 'linked-claims error: credentialSubject.type must include EndorsementSubject'\n );\n }\n if (!csObj?.id) {\n check.errors.push('linked-claims error: credentialSubject.id missing');\n }\n\n if (!check.errors.length) check.checks.push('linked-claims');\n return check;\n },\n\n storeEndorsement: async (_learnCard, endorsement, options) => {\n const storeName = pickStoreProvider(learnCard, options?.storeName);\n if (!storeName) throw new Error('No store plane provider available');\n if (\n !learnCard.store[storeName].uploadEncrypted ||\n typeof learnCard.store[storeName].uploadEncrypted !== 'function'\n ) {\n throw new Error(\n `Store provider '${storeName}' does not support uploadEncrypted method`\n );\n }\n const uri: string = await (learnCard.store as any)[storeName].uploadEncrypted(\n endorsement\n );\n\n const indexName = pickIndexProvider(learnCard, options?.indexName);\n let indexed = false;\n // prettier-ignore\n let id =\n (endorsement as any).id ||\n `urn:uuid:${globalThis.crypto?.randomUUID?.() ||\n (() => {\n throw new Error(\n 'Secure random UUID generation is not available. Please use an environment that supports crypto.randomUUID().'\n );\n })()\n }`;\n\n if (indexName) {\n const cs: any = (endorsement as any).credentialSubject;\n const csObj = Array.isArray(cs) ? cs[0] : cs;\n const record = {\n id,\n uri,\n type: ensureArray((endorsement as any).type),\n endorsedId: csObj?.id,\n subjectId: csObj?.id,\n originalCredentialId: csObj?.id,\n issuedOn: (endorsement as any).validFrom || (endorsement as any).issuanceDate,\n category: 'Endorsement',\n title: endorsement.name,\n credentialId: options?.credentialId ?? csObj?.id,\n sharedUri: options?.sharedUri, // original credential shared uri\n relationship: options?.relationship,\n visibility: options?.visibility ?? 'public',\n };\n indexed = await (learnCard.index as any)[indexName].add(record);\n }\n\n return { uri, indexed, id, indexName, storeName };\n },\n\n getEndorsements: async (_learnCard, original, options) => {\n const indexName = pickIndexProvider(learnCard, options?.indexName);\n if (!indexName) return [];\n\n if (!original.id) return [];\n\n const query = { originalCredentialId: original.id };\n const records = await (learnCard.index as any)[indexName].get(query);\n\n const results: VC[] = [];\n for (const rec of records) {\n const vc = await learnCard.read.get(rec.uri);\n if (vc) results.push(vc as VC);\n }\n return results;\n },\n },\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["index_exports", "__export", "getLinkedClaimsPlugin", "__toCommonJS", "ENDORSEMENT_CONTEXT", "pickIndexProvider", "__name", "learnCard", "preferred", "providers", "k", "pickStoreProvider", "ensureArray", "arr", "getLinkedClaimsPlugin", "_learnCard", "original", "details", "opts", "issuer", "targetId", "unsigned", "endorsement", "options", "check", "ctx", "types", "c", "cs", "csObj", "storeName", "uri", "indexName", "indexed", "id", "record", "query", "records", "results", "rec", "vc"]
|
|
7
7
|
}
|
|
@@ -3,11 +3,6 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
var ENDORSEMENT_CONTEXT = "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json";
|
|
6
|
-
var getSubjectIds = /* @__PURE__ */ __name((credential) => {
|
|
7
|
-
const subject = credential.credentialSubject;
|
|
8
|
-
if (Array.isArray(subject)) return subject.map((s) => s?.id).filter(Boolean);
|
|
9
|
-
return subject?.id ? [subject.id] : [];
|
|
10
|
-
}, "getSubjectIds");
|
|
11
6
|
var pickIndexProvider = /* @__PURE__ */ __name((learnCard, preferred) => {
|
|
12
7
|
const providers = Object.keys(learnCard.index || {}).filter(
|
|
13
8
|
(k) => !["providers", "all"].includes(k)
|
|
@@ -31,11 +26,8 @@ var getLinkedClaimsPlugin = /* @__PURE__ */ __name((learnCard) => ({
|
|
|
31
26
|
methods: {
|
|
32
27
|
endorseCredential: /* @__PURE__ */ __name(async (_learnCard, original, details, opts) => {
|
|
33
28
|
const issuer = learnCard.id.did();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const targetId = originalId || subjectIds[0];
|
|
37
|
-
if (!targetId)
|
|
38
|
-
throw new Error("Original credential must have either id or credentialSubject.id");
|
|
29
|
+
const targetId = original.id;
|
|
30
|
+
if (!targetId) throw new Error("Original credential must have an id");
|
|
39
31
|
const context = [
|
|
40
32
|
"https://www.w3.org/ns/credentials/v2",
|
|
41
33
|
ENDORSEMENT_CONTEXT
|
|
@@ -111,8 +103,8 @@ var getLinkedClaimsPlugin = /* @__PURE__ */ __name((learnCard) => ({
|
|
|
111
103
|
originalCredentialId: csObj?.id,
|
|
112
104
|
issuedOn: endorsement.validFrom || endorsement.issuanceDate,
|
|
113
105
|
category: "Endorsement",
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
title: endorsement.name,
|
|
107
|
+
credentialId: options?.credentialId ?? csObj?.id,
|
|
116
108
|
sharedUri: options?.sharedUri,
|
|
117
109
|
// original credential shared uri
|
|
118
110
|
relationship: options?.relationship,
|
|
@@ -125,9 +117,8 @@ var getLinkedClaimsPlugin = /* @__PURE__ */ __name((learnCard) => ({
|
|
|
125
117
|
getEndorsements: /* @__PURE__ */ __name(async (_learnCard, original, options) => {
|
|
126
118
|
const indexName = pickIndexProvider(learnCard, options?.indexName);
|
|
127
119
|
if (!indexName) return [];
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const query = originalId ? { originalCredentialId: originalId } : { endorsedId: subjectIds[0] };
|
|
120
|
+
if (!original.id) return [];
|
|
121
|
+
const query = { originalCredentialId: original.id };
|
|
131
122
|
const records = await learnCard.index[indexName].get(query);
|
|
132
123
|
const results = [];
|
|
133
124
|
for (const rec of records) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { LearnCard } from '@learncard/core';\nimport type { VC, UnsignedVC } from '@learncard/types';\nimport type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';\n\nexport * from './types';\n\nconst ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';\n\nconst
|
|
5
|
-
"mappings": ";;;;AAMA,IAAM,sBAAsB;AAE5B,IAAM,
|
|
4
|
+
"sourcesContent": ["import type { LearnCard } from '@learncard/core';\nimport type { VC, UnsignedVC } from '@learncard/types';\nimport type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';\n\nexport * from './types';\n\nconst ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';\n\nconst pickIndexProvider = (learnCard: any, preferred?: string): string | undefined => {\n const providers = Object.keys(learnCard.index || {}).filter(\n k => !['providers', 'all'].includes(k)\n );\n if (!providers.length) return undefined;\n if (preferred && providers.includes(preferred)) return preferred;\n if (providers.includes('LearnCloud')) return 'LearnCloud';\n return providers[0];\n};\n\nconst pickStoreProvider = (learnCard: any, preferred?: string): string | undefined => {\n const providers = Object.keys(learnCard.store || {}).filter(k => k !== 'providers');\n if (!providers.length) return undefined;\n if (preferred && providers.includes(preferred)) return preferred;\n return providers[0];\n};\n\nconst ensureArray = <T>(arr: T | T[] | undefined): T[] =>\n Array.isArray(arr) ? arr : arr ? [arr] : [];\n\nexport const getLinkedClaimsPlugin = (\n learnCard: LearnCard<any, 'id' | 'store' | 'index' | 'read', LinkedClaimsPluginDependentMethods>\n): LinkedClaimsPlugin => ({\n name: 'LinkedClaims',\n displayName: 'LinkedClaims',\n description: 'Create, verify, store, and retrieve endorsement credentials linked to originals.',\n methods: {\n endorseCredential: async (_learnCard, original, details, opts) => {\n const issuer = learnCard.id.did();\n\n const targetId = original.id;\n if (!targetId) throw new Error('Original credential must have an id');\n\n const context: (string | Record<string, any>)[] = [\n 'https://www.w3.org/ns/credentials/v2',\n ENDORSEMENT_CONTEXT,\n ];\n\n const types: [string, ...string[]] = ['VerifiableCredential', 'EndorsementCredential'];\n\n const unsigned: UnsignedVC = {\n '@context': context as any,\n type: types,\n issuer,\n validFrom: new Date().toISOString(),\n credentialSubject: {\n type: ['EndorsementSubject'],\n id: targetId,\n ...(details.endorsementComment\n ? { endorsementComment: details.endorsementComment }\n : {}),\n },\n ...(details.name ? { name: details.name } : { name: `Endorsement of ${targetId}` }),\n ...(details.description ? { description: details.description } : {}),\n ...(details.evidence && { evidence: details.evidence as any }),\n };\n\n return learnCard.invoke.issueCredential(unsigned);\n },\n\n verifyEndorsement: async (_learnCard, endorsement, options) => {\n const check = await learnCard.invoke.verifyCredential(endorsement, options);\n\n const ctx = ensureArray((endorsement as any)['@context']);\n const types = ensureArray((endorsement as any)['type']);\n\n if (\n !ctx.some((c: string) => c === 'https://www.w3.org/ns/credentials/v2') ||\n !ctx.some((c: string) => c === ENDORSEMENT_CONTEXT)\n ) {\n check.errors.push('linked-claims error: missing VCv2 or OBv3 context');\n }\n if (!types.includes('EndorsementCredential')) {\n check.errors.push('linked-claims error: missing EndorsementCredential type');\n }\n\n const cs: any = (endorsement as any).credentialSubject;\n const csObj = Array.isArray(cs) ? cs[0] : cs;\n const csTypes = ensureArray<string>(csObj?.type);\n if (!csTypes.includes('EndorsementSubject')) {\n check.errors.push(\n 'linked-claims error: credentialSubject.type must include EndorsementSubject'\n );\n }\n if (!csObj?.id) {\n check.errors.push('linked-claims error: credentialSubject.id missing');\n }\n\n if (!check.errors.length) check.checks.push('linked-claims');\n return check;\n },\n\n storeEndorsement: async (_learnCard, endorsement, options) => {\n const storeName = pickStoreProvider(learnCard, options?.storeName);\n if (!storeName) throw new Error('No store plane provider available');\n if (\n !learnCard.store[storeName].uploadEncrypted ||\n typeof learnCard.store[storeName].uploadEncrypted !== 'function'\n ) {\n throw new Error(\n `Store provider '${storeName}' does not support uploadEncrypted method`\n );\n }\n const uri: string = await (learnCard.store as any)[storeName].uploadEncrypted(\n endorsement\n );\n\n const indexName = pickIndexProvider(learnCard, options?.indexName);\n let indexed = false;\n // prettier-ignore\n let id =\n (endorsement as any).id ||\n `urn:uuid:${globalThis.crypto?.randomUUID?.() ||\n (() => {\n throw new Error(\n 'Secure random UUID generation is not available. Please use an environment that supports crypto.randomUUID().'\n );\n })()\n }`;\n\n if (indexName) {\n const cs: any = (endorsement as any).credentialSubject;\n const csObj = Array.isArray(cs) ? cs[0] : cs;\n const record = {\n id,\n uri,\n type: ensureArray((endorsement as any).type),\n endorsedId: csObj?.id,\n subjectId: csObj?.id,\n originalCredentialId: csObj?.id,\n issuedOn: (endorsement as any).validFrom || (endorsement as any).issuanceDate,\n category: 'Endorsement',\n title: endorsement.name,\n credentialId: options?.credentialId ?? csObj?.id,\n sharedUri: options?.sharedUri, // original credential shared uri\n relationship: options?.relationship,\n visibility: options?.visibility ?? 'public',\n };\n indexed = await (learnCard.index as any)[indexName].add(record);\n }\n\n return { uri, indexed, id, indexName, storeName };\n },\n\n getEndorsements: async (_learnCard, original, options) => {\n const indexName = pickIndexProvider(learnCard, options?.indexName);\n if (!indexName) return [];\n\n if (!original.id) return [];\n\n const query = { originalCredentialId: original.id };\n const records = await (learnCard.index as any)[indexName].get(query);\n\n const results: VC[] = [];\n for (const rec of records) {\n const vc = await learnCard.read.get(rec.uri);\n if (vc) results.push(vc as VC);\n }\n return results;\n },\n },\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-endorsement.test.d.ts","sourceRoot":"","sources":["../../src/test/store-endorsement.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/linked-claims-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"description": "LinkedClaims plugin to create, verify, store, and query endorsement credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
},
|
|
48
48
|
"types": "./dist/index.d.ts",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@learncard/core": "9.4.
|
|
51
|
-
"@learncard/types": "5.
|
|
52
|
-
"@learncard/vc-plugin": "^1.6.
|
|
50
|
+
"@learncard/core": "9.4.36",
|
|
51
|
+
"@learncard/types": "5.21.0",
|
|
52
|
+
"@learncard/vc-plugin": "^1.6.2",
|
|
53
53
|
"why-is-node-running": "^2.2.2"
|
|
54
54
|
},
|
|
55
55
|
"sideEffects": false
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,6 @@ export * from './types';
|
|
|
6
6
|
|
|
7
7
|
const ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';
|
|
8
8
|
|
|
9
|
-
const getSubjectIds = (credential: VC): string[] => {
|
|
10
|
-
const subject = credential.credentialSubject as any;
|
|
11
|
-
if (Array.isArray(subject)) return subject.map(s => s?.id).filter(Boolean);
|
|
12
|
-
return subject?.id ? [subject.id] : [];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
9
|
const pickIndexProvider = (learnCard: any, preferred?: string): string | undefined => {
|
|
16
10
|
const providers = Object.keys(learnCard.index || {}).filter(
|
|
17
11
|
k => !['providers', 'all'].includes(k)
|
|
@@ -42,11 +36,8 @@ export const getLinkedClaimsPlugin = (
|
|
|
42
36
|
endorseCredential: async (_learnCard, original, details, opts) => {
|
|
43
37
|
const issuer = learnCard.id.did();
|
|
44
38
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const targetId = originalId || subjectIds[0];
|
|
48
|
-
if (!targetId)
|
|
49
|
-
throw new Error('Original credential must have either id or credentialSubject.id');
|
|
39
|
+
const targetId = original.id;
|
|
40
|
+
if (!targetId) throw new Error('Original credential must have an id');
|
|
50
41
|
|
|
51
42
|
const context: (string | Record<string, any>)[] = [
|
|
52
43
|
'https://www.w3.org/ns/credentials/v2',
|
|
@@ -124,9 +115,10 @@ export const getLinkedClaimsPlugin = (
|
|
|
124
115
|
|
|
125
116
|
const indexName = pickIndexProvider(learnCard, options?.indexName);
|
|
126
117
|
let indexed = false;
|
|
118
|
+
// prettier-ignore
|
|
127
119
|
let id =
|
|
128
120
|
(endorsement as any).id ||
|
|
129
|
-
`urn:uuid:${
|
|
121
|
+
`urn:uuid:${globalThis.crypto?.randomUUID?.() ||
|
|
130
122
|
(() => {
|
|
131
123
|
throw new Error(
|
|
132
124
|
'Secure random UUID generation is not available. Please use an environment that supports crypto.randomUUID().'
|
|
@@ -146,7 +138,8 @@ export const getLinkedClaimsPlugin = (
|
|
|
146
138
|
originalCredentialId: csObj?.id,
|
|
147
139
|
issuedOn: (endorsement as any).validFrom || (endorsement as any).issuanceDate,
|
|
148
140
|
category: 'Endorsement',
|
|
149
|
-
|
|
141
|
+
title: endorsement.name,
|
|
142
|
+
credentialId: options?.credentialId ?? csObj?.id,
|
|
150
143
|
sharedUri: options?.sharedUri, // original credential shared uri
|
|
151
144
|
relationship: options?.relationship,
|
|
152
145
|
visibility: options?.visibility ?? 'public',
|
|
@@ -161,12 +154,9 @@ export const getLinkedClaimsPlugin = (
|
|
|
161
154
|
const indexName = pickIndexProvider(learnCard, options?.indexName);
|
|
162
155
|
if (!indexName) return [];
|
|
163
156
|
|
|
164
|
-
|
|
165
|
-
const originalId = (original as any).id;
|
|
157
|
+
if (!original.id) return [];
|
|
166
158
|
|
|
167
|
-
const query =
|
|
168
|
-
? { originalCredentialId: originalId }
|
|
169
|
-
: { endorsedId: subjectIds[0] };
|
|
159
|
+
const query = { originalCredentialId: original.id };
|
|
170
160
|
const records = await (learnCard.index as any)[indexName].get(query);
|
|
171
161
|
|
|
172
162
|
const results: VC[] = [];
|
|
@@ -1,205 +1,258 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- legacy plugin mocks intentionally model flexible LearnCard surfaces. */
|
|
1
2
|
import { getLinkedClaimsPlugin } from '../index';
|
|
2
3
|
import type { VC, VerificationCheck } from '@learncard/types';
|
|
3
4
|
|
|
4
5
|
// Minimal mock LearnCard with just the surfaces the plugin uses
|
|
5
6
|
const createMockLearnCard = () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
7
|
+
const mock = {
|
|
8
|
+
id: { did: () => 'did:example:issuer' },
|
|
9
|
+
invoke: {
|
|
10
|
+
issueCredential: async (unsigned: any): Promise<VC> =>
|
|
11
|
+
({
|
|
12
|
+
id: 'urn:uuid:endorsement-1',
|
|
13
|
+
...unsigned,
|
|
14
|
+
}) as any,
|
|
15
|
+
verifyCredential: async (_vc: VC, _opts?: any): Promise<VerificationCheck> => ({
|
|
16
|
+
checks: [],
|
|
17
|
+
warnings: [],
|
|
18
|
+
errors: [],
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
// Not used in these tests, but kept for completeness
|
|
22
|
+
store: { TestStore: { uploadEncrypted: async (_: any) => 'lc://test-uri' } },
|
|
23
|
+
index: {
|
|
24
|
+
LearnCloud: {
|
|
25
|
+
add: async (_: any) => true,
|
|
26
|
+
get: async (_: any) => [],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
read: { get: async (_: string) => undefined },
|
|
30
|
+
} as unknown as any;
|
|
31
|
+
|
|
32
|
+
return mock;
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const VC_V2 = 'https://www.w3.org/ns/credentials/v2';
|
|
34
36
|
const OBV3 = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';
|
|
35
37
|
|
|
36
|
-
const makeOriginalWithId = (): VC =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
const makeOriginalWithId = (): VC =>
|
|
39
|
+
({
|
|
40
|
+
'@context': [VC_V2],
|
|
41
|
+
type: ['VerifiableCredential'],
|
|
42
|
+
issuer: 'did:example:someone',
|
|
43
|
+
validFrom: new Date().toISOString(),
|
|
44
|
+
credentialSubject: { id: 'did:example:subject-123' },
|
|
45
|
+
id: 'urn:uuid:original-123',
|
|
46
|
+
}) as any;
|
|
47
|
+
|
|
48
|
+
const makeOriginalWithoutIdButSubject = (): VC =>
|
|
49
|
+
({
|
|
50
|
+
'@context': [VC_V2],
|
|
51
|
+
type: ['VerifiableCredential'],
|
|
52
|
+
issuer: 'did:example:someone',
|
|
53
|
+
validFrom: new Date().toISOString(),
|
|
54
|
+
credentialSubject: { id: 'did:example:subject-abc' },
|
|
55
|
+
}) as any;
|
|
52
56
|
|
|
53
57
|
const clone = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
|
54
58
|
|
|
55
59
|
describe('LinkedClaims Plugin', () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
60
|
+
test('endorseCredential: minimal (comment only) with original id', async () => {
|
|
61
|
+
const lc = createMockLearnCard();
|
|
62
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
63
|
+
|
|
64
|
+
const original = makeOriginalWithId();
|
|
65
|
+
|
|
66
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
67
|
+
lc,
|
|
68
|
+
original,
|
|
69
|
+
{ endorsementComment: 'Solid work' },
|
|
70
|
+
{}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const ctx = endorsement['@context'] as any[];
|
|
74
|
+
expect(Array.isArray(ctx)).toBe(true);
|
|
75
|
+
expect(ctx).toEqual(expect.arrayContaining([VC_V2, OBV3]));
|
|
76
|
+
|
|
77
|
+
const types = endorsement.type as string[];
|
|
78
|
+
expect(types).toEqual(
|
|
79
|
+
expect.arrayContaining(['VerifiableCredential', 'EndorsementCredential'])
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect((endorsement as any).issuer).toBe('did:example:issuer');
|
|
83
|
+
|
|
84
|
+
const cs = (endorsement as any).credentialSubject;
|
|
85
|
+
const csObj = Array.isArray(cs) ? cs[0] : cs;
|
|
86
|
+
expect(csObj.id).toBe('urn:uuid:original-123'); // prefers original.id when present
|
|
87
|
+
expect(csObj.type).toEqual(expect.arrayContaining(['EndorsementSubject']));
|
|
88
|
+
expect(csObj.endorsementComment).toBe('Solid work');
|
|
89
|
+
|
|
90
|
+
// default name applied when not provided
|
|
91
|
+
expect((endorsement as any).name).toBe('Endorsement of urn:uuid:original-123');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('uses the endorsing wallet DID instead of the target identity as issuer', async () => {
|
|
95
|
+
const lc = createMockLearnCard();
|
|
96
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
97
|
+
const target = {
|
|
98
|
+
...makeOriginalWithId(),
|
|
99
|
+
id: `urn:sha256:${'a'.repeat(64)}`,
|
|
100
|
+
issuer: `urn:sha256:${'b'.repeat(64)}`,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
104
|
+
lc,
|
|
105
|
+
target,
|
|
106
|
+
{ endorsementComment: 'Solid work' },
|
|
107
|
+
{}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(endorsement.issuer).toBe('did:example:issuer');
|
|
111
|
+
expect((endorsement.credentialSubject as { id?: string }).id).toBe(target.id);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('endorseCredential: rejects a subject DID as a credential target', async () => {
|
|
115
|
+
const lc = createMockLearnCard();
|
|
116
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
117
|
+
const original = makeOriginalWithoutIdButSubject();
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
plugin.methods.endorseCredential(
|
|
121
|
+
lc,
|
|
122
|
+
original,
|
|
123
|
+
{ name: 'Kudos', description: 'Peer endorsement' },
|
|
124
|
+
{}
|
|
125
|
+
)
|
|
126
|
+
).rejects.toThrow('Original credential must have an id');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('endorseCredential: throws if original lacks id and credentialSubject.id', async () => {
|
|
130
|
+
const lc = createMockLearnCard();
|
|
131
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
132
|
+
|
|
133
|
+
const original = {
|
|
134
|
+
'@context': [VC_V2],
|
|
135
|
+
type: ['VerifiableCredential'],
|
|
136
|
+
issuer: 'did:example:none',
|
|
137
|
+
validFrom: new Date().toISOString(),
|
|
138
|
+
credentialSubject: {},
|
|
139
|
+
} as any as VC;
|
|
140
|
+
|
|
141
|
+
await expect(
|
|
142
|
+
plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {})
|
|
143
|
+
).rejects.toThrow('Original credential must have an id');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('verifyEndorsement: valid endorsement passes and adds linked-claims check', async () => {
|
|
147
|
+
const lc = createMockLearnCard();
|
|
148
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
149
|
+
const original = makeOriginalWithId();
|
|
150
|
+
|
|
151
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
152
|
+
lc,
|
|
153
|
+
original,
|
|
154
|
+
{ endorsementComment: 'Nice' },
|
|
155
|
+
{}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const result = await plugin.methods.verifyEndorsement(lc, endorsement, {});
|
|
159
|
+
expect(result.errors).toHaveLength(0);
|
|
160
|
+
expect(result.checks).toEqual(expect.arrayContaining(['linked-claims']));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('verifyEndorsement: missing OBv3 context -> error', async () => {
|
|
164
|
+
const lc = createMockLearnCard();
|
|
165
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
166
|
+
const original = makeOriginalWithId();
|
|
167
|
+
|
|
168
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
169
|
+
lc,
|
|
170
|
+
original,
|
|
171
|
+
{ endorsementComment: 'x' },
|
|
172
|
+
{}
|
|
173
|
+
);
|
|
174
|
+
const bad = clone(endorsement);
|
|
175
|
+
(bad as any)['@context'] = [VC_V2];
|
|
176
|
+
|
|
177
|
+
const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
|
|
178
|
+
expect(res.errors.join(' ')).toMatch(/missing VCv2 or OBv3 context/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('verifyEndorsement: missing VC v2 context -> error', async () => {
|
|
182
|
+
const lc = createMockLearnCard();
|
|
183
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
184
|
+
const original = makeOriginalWithId();
|
|
185
|
+
|
|
186
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
187
|
+
lc,
|
|
188
|
+
original,
|
|
189
|
+
{ endorsementComment: 'x' },
|
|
190
|
+
{}
|
|
191
|
+
);
|
|
192
|
+
const bad = clone(endorsement);
|
|
193
|
+
(bad as any)['@context'] = [OBV3];
|
|
194
|
+
|
|
195
|
+
const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
|
|
196
|
+
expect(res.errors.join(' ')).toMatch(/missing VCv2 or OBv3 context/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('verifyEndorsement: missing EndorsementCredential type -> error', async () => {
|
|
200
|
+
const lc = createMockLearnCard();
|
|
201
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
202
|
+
const original = makeOriginalWithId();
|
|
203
|
+
|
|
204
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
205
|
+
lc,
|
|
206
|
+
original,
|
|
207
|
+
{ endorsementComment: 'x' },
|
|
208
|
+
{}
|
|
209
|
+
);
|
|
210
|
+
const bad = clone(endorsement);
|
|
211
|
+
(bad as any).type = ['VerifiableCredential'];
|
|
212
|
+
|
|
213
|
+
const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
|
|
214
|
+
expect(res.errors.join(' ')).toMatch(/missing EndorsementCredential type/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('verifyEndorsement: subject type missing EndorsementSubject -> error', async () => {
|
|
218
|
+
const lc = createMockLearnCard();
|
|
219
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
220
|
+
const original = makeOriginalWithId();
|
|
221
|
+
|
|
222
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
223
|
+
lc,
|
|
224
|
+
original,
|
|
225
|
+
{ endorsementComment: 'x' },
|
|
226
|
+
{}
|
|
227
|
+
);
|
|
228
|
+
const bad = clone(endorsement);
|
|
229
|
+
const cs = (bad as any).credentialSubject;
|
|
230
|
+
const csObj = Array.isArray(cs) ? cs[0] : cs;
|
|
231
|
+
csObj.type = ['SomeOtherType'];
|
|
232
|
+
|
|
233
|
+
const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
|
|
234
|
+
expect(res.errors.join(' ')).toMatch(
|
|
235
|
+
/credentialSubject\.type must include EndorsementSubject/
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('verifyEndorsement: missing credentialSubject.id -> error', async () => {
|
|
240
|
+
const lc = createMockLearnCard();
|
|
241
|
+
const plugin = getLinkedClaimsPlugin(lc);
|
|
242
|
+
const original = makeOriginalWithId();
|
|
243
|
+
|
|
244
|
+
const endorsement = await plugin.methods.endorseCredential(
|
|
245
|
+
lc,
|
|
246
|
+
original,
|
|
247
|
+
{ endorsementComment: 'x' },
|
|
248
|
+
{}
|
|
249
|
+
);
|
|
250
|
+
const bad = clone(endorsement);
|
|
251
|
+
const cs = (bad as any).credentialSubject;
|
|
252
|
+
const csObj = Array.isArray(cs) ? cs[0] : cs;
|
|
253
|
+
delete csObj.id;
|
|
254
|
+
|
|
255
|
+
const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
|
|
256
|
+
expect(res.errors.join(' ')).toMatch(/credentialSubject\.id missing/);
|
|
257
|
+
});
|
|
205
258
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { VC } from '@learncard/types';
|
|
3
|
+
|
|
4
|
+
import { getLinkedClaimsPlugin } from '../index';
|
|
5
|
+
import type { LinkedClaimsPlugin } from '../types';
|
|
6
|
+
|
|
7
|
+
type LinkedClaimsLearnCard = Parameters<LinkedClaimsPlugin['methods']['storeEndorsement']>[0];
|
|
8
|
+
|
|
9
|
+
const endorsement = {
|
|
10
|
+
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
11
|
+
type: ['VerifiableCredential', 'EndorsementCredential'],
|
|
12
|
+
issuer: 'did:example:endorser',
|
|
13
|
+
name: 'Endorsement of First Aid',
|
|
14
|
+
credentialSubject: {
|
|
15
|
+
id: 'urn:uuid:original-123',
|
|
16
|
+
type: ['EndorsementSubject'],
|
|
17
|
+
},
|
|
18
|
+
} as unknown as VC;
|
|
19
|
+
|
|
20
|
+
const createTestContext = () => {
|
|
21
|
+
const add = vi.fn().mockResolvedValue(true);
|
|
22
|
+
const learnCard = {
|
|
23
|
+
store: {
|
|
24
|
+
LearnCloud: {
|
|
25
|
+
uploadEncrypted: vi.fn().mockResolvedValue('lc:endorsement:1'),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
index: { LearnCloud: { add } },
|
|
29
|
+
} as unknown as LinkedClaimsLearnCard;
|
|
30
|
+
|
|
31
|
+
return { add, learnCard };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('storeEndorsement', () => {
|
|
35
|
+
it('indexes the signed target when metadata omits credentialId', async () => {
|
|
36
|
+
const { add, learnCard } = createTestContext();
|
|
37
|
+
const plugin = getLinkedClaimsPlugin(learnCard);
|
|
38
|
+
|
|
39
|
+
await plugin.methods.storeEndorsement(learnCard, endorsement, {});
|
|
40
|
+
|
|
41
|
+
expect(add).toHaveBeenCalledWith(
|
|
42
|
+
expect.objectContaining({
|
|
43
|
+
credentialId: 'urn:uuid:original-123',
|
|
44
|
+
originalCredentialId: 'urn:uuid:original-123',
|
|
45
|
+
title: 'Endorsement of First Aid',
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('preserves an explicit wrapper credential id', async () => {
|
|
51
|
+
const { add, learnCard } = createTestContext();
|
|
52
|
+
const plugin = getLinkedClaimsPlugin(learnCard);
|
|
53
|
+
|
|
54
|
+
await plugin.methods.storeEndorsement(learnCard, endorsement, {
|
|
55
|
+
credentialId: 'urn:uuid:wrapper-credential',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(add).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
credentialId: 'urn:uuid:wrapper-credential',
|
|
61
|
+
originalCredentialId: 'urn:uuid:original-123',
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|