@learncard/linked-claims-plugin 0.2.34 → 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 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 credentials by subject id or original credential id.
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: `Endorsement of ${originalVc.id ?? originalVc.credentialSubject?.id}`,
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
- - `endorsementComment?: string`
48
- - `name?: string`
49
- - `description?: string`
50
+ - `endorsementComment?: string`
51
+ - `name?: string`
52
+ - `description?: string`
@@ -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;AA8BxB,eAAO,MAAM,qBAAqB,GAC9B,WAAW,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,EAAE,kCAAkC,CAAC,KACjG,kBA+ID,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 originalId = original.id;
57
- const subjectIds = getSubjectIds(original);
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
- credentialId: options?.credentialId,
137
- // original credential id
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
- const subjectIds = getSubjectIds(original);
151
- const originalId = original.id;
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 getSubjectIds = (credential: VC): string[] => {\n const subject = credential.credentialSubject as any;\n if (Array.isArray(subject)) return subject.map(s => s?.id).filter(Boolean);\n return subject?.id ? [subject.id] : [];\n};\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 originalId: string | undefined = (original as any).id;\n const subjectIds = getSubjectIds(original);\n const targetId = originalId || subjectIds[0];\n if (!targetId)\n throw new Error('Original credential must have either id or credentialSubject.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 let id =\n (endorsement as any).id ||\n `urn:uuid:${(globalThis as any).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 credentialId: options?.credentialId, // original credential 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 const subjectIds = getSubjectIds(original);\n const originalId = (original as any).id;\n\n const query = originalId\n ? { originalCredentialId: originalId }\n : { endorsedId: subjectIds[0] };\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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",
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 p=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var c=(e,t)=>p(e,"name",{value:t,configurable:!0});var E=(e,t)=>{for(var n in t)p(e,n,{get:t[n],enumerable:!0})},w=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of b(t))!C.call(e,i)&&i!==n&&p(e,i,{get:()=>t[i],enumerable:!(r=h(t,i))||r.enumerable});return e};var k=e=>w(p({},"__esModule",{value:!0}),e);var I={};E(I,{getLinkedClaimsPlugin:()=>x});module.exports=k(I);var g="https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",f=c(e=>{let t=e.credentialSubject;return Array.isArray(t)?t.map(n=>n?.id).filter(Boolean):t?.id?[t.id]:[]},"getSubjectIds"),v=c((e,t)=>{let n=Object.keys(e.index||{}).filter(r=>!["providers","all"].includes(r));if(n.length)return t&&n.includes(t)?t:n.includes("LearnCloud")?"LearnCloud":n[0]},"pickIndexProvider"),j=c((e,t)=>{let n=Object.keys(e.store||{}).filter(r=>r!=="providers");if(n.length)return t&&n.includes(t)?t:n[0]},"pickStoreProvider"),m=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(t,n,r,i)=>{let l=e.id.did(),o=n.id,d=f(n),s=o||d[0];if(!s)throw new Error("Original credential must have either id or credentialSubject.id");let y={"@context":["https://www.w3.org/ns/credentials/v2",g],type:["VerifiableCredential","EndorsementCredential"],issuer:l,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(y)},"endorseCredential"),verifyEndorsement:c(async(t,n,r)=>{let i=await e.invoke.verifyCredential(n,r),l=m(n["@context"]),o=m(n.type);(!l.some(a=>a==="https://www.w3.org/ns/credentials/v2")||!l.some(a=>a===g))&&i.errors.push("linked-claims error: missing VCv2 or OBv3 context"),o.includes("EndorsementCredential")||i.errors.push("linked-claims error: missing EndorsementCredential type");let d=n.credentialSubject,s=Array.isArray(d)?d[0]:d;return m(s?.type).includes("EndorsementSubject")||i.errors.push("linked-claims error: credentialSubject.type must include EndorsementSubject"),s?.id||i.errors.push("linked-claims error: credentialSubject.id missing"),i.errors.length||i.checks.push("linked-claims"),i},"verifyEndorsement"),storeEndorsement:c(async(t,n,r)=>{let i=j(e,r?.storeName);if(!i)throw new Error("No store plane provider available");if(!e.store[i].uploadEncrypted||typeof e.store[i].uploadEncrypted!="function")throw new Error(`Store provider '${i}' does not support uploadEncrypted method`);let l=await e.store[i].uploadEncrypted(n),o=v(e,r?.indexName),d=!1,s=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(o){let u=n.credentialSubject,a=Array.isArray(u)?u[0]:u,y={id:s,uri:l,type:m(n.type),endorsedId:a?.id,subjectId:a?.id,originalCredentialId:a?.id,issuedOn:n.validFrom||n.issuanceDate,category:"Endorsement",credentialId:r?.credentialId,sharedUri:r?.sharedUri,relationship:r?.relationship,visibility:r?.visibility??"public"};d=await e.index[o].add(y)}return{uri:l,indexed:d,id:s,indexName:o,storeName:i}},"storeEndorsement"),getEndorsements:c(async(t,n,r)=>{let i=v(e,r?.indexName);if(!i)return[];let l=f(n),o=n.id,d=o?{originalCredentialId:o}:{endorsedId:l[0]},s=await e.index[i].get(d),u=[];for(let a of s){let y=await e.read.get(a.uri);y&&u.push(y)}return u},"getEndorsements")}}),"getLinkedClaimsPlugin");
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 getSubjectIds = (credential: VC): string[] => {\n const subject = credential.credentialSubject as any;\n if (Array.isArray(subject)) return subject.map(s => s?.id).filter(Boolean);\n return subject?.id ? [subject.id] : [];\n};\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 originalId: string | undefined = (original as any).id;\n const subjectIds = getSubjectIds(original);\n const targetId = originalId || subjectIds[0];\n if (!targetId)\n throw new Error('Original credential must have either id or credentialSubject.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 let id =\n (endorsement as any).id ||\n `urn:uuid:${(globalThis as any).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 credentialId: options?.credentialId, // original credential 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 const subjectIds = getSubjectIds(original);\n const originalId = (original as any).id;\n\n const query = originalId\n ? { originalCredentialId: originalId }\n : { endorsedId: subjectIds[0] };\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", "getSubjectIds", "__name", "credential", "subject", "s", "pickIndexProvider", "learnCard", "preferred", "providers", "k", "pickStoreProvider", "ensureArray", "arr", "getLinkedClaimsPlugin", "_learnCard", "original", "details", "opts", "issuer", "originalId", "subjectIds", "targetId", "unsigned", "endorsement", "options", "check", "ctx", "types", "c", "cs", "csObj", "storeName", "uri", "indexName", "indexed", "id", "record", "query", "records", "results", "rec", "vc"]
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 originalId = original.id;
35
- const subjectIds = getSubjectIds(original);
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
- credentialId: options?.credentialId,
115
- // original credential id
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
- const subjectIds = getSubjectIds(original);
129
- const originalId = original.id;
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 getSubjectIds = (credential: VC): string[] => {\n const subject = credential.credentialSubject as any;\n if (Array.isArray(subject)) return subject.map(s => s?.id).filter(Boolean);\n return subject?.id ? [subject.id] : [];\n};\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 originalId: string | undefined = (original as any).id;\n const subjectIds = getSubjectIds(original);\n const targetId = originalId || subjectIds[0];\n if (!targetId)\n throw new Error('Original credential must have either id or credentialSubject.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 let id =\n (endorsement as any).id ||\n `urn:uuid:${(globalThis as any).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 credentialId: options?.credentialId, // original credential 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 const subjectIds = getSubjectIds(original);\n const originalId = (original as any).id;\n\n const query = originalId\n ? { originalCredentialId: originalId }\n : { endorsedId: subjectIds[0] };\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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",
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store-endorsement.test.d.ts.map
@@ -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.34",
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.34",
51
- "@learncard/types": "5.19.0",
52
- "@learncard/vc-plugin": "^1.6.0",
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 originalId: string | undefined = (original as any).id;
46
- const subjectIds = getSubjectIds(original);
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:${(globalThis as any).crypto?.randomUUID?.() ||
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
- credentialId: options?.credentialId, // original credential id
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
- const subjectIds = getSubjectIds(original);
165
- const originalId = (original as any).id;
157
+ if (!original.id) return [];
166
158
 
167
- const query = originalId
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
- const mock = {
7
- id: { did: () => 'did:example:issuer' },
8
- invoke: {
9
- issueCredential: async (unsigned: any): Promise<VC> => ({
10
- id: 'urn:uuid:endorsement-1',
11
- ...unsigned,
12
- }) as any,
13
- verifyCredential: async (_vc: VC, _opts?: any): Promise<VerificationCheck> => ({
14
- checks: [],
15
- warnings: [],
16
- errors: [],
17
- }),
18
- },
19
- // Not used in these tests, but kept for completeness
20
- store: { TestStore: { uploadEncrypted: async (_: any) => 'lc://test-uri' } },
21
- index: {
22
- LearnCloud: {
23
- add: async (_: any) => true,
24
- get: async (_: any) => [],
25
- },
26
- },
27
- read: { get: async (_: string) => undefined },
28
- } as unknown as any;
29
-
30
- return mock;
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
- '@context': [VC_V2],
38
- type: ['VerifiableCredential'],
39
- issuer: 'did:example:someone',
40
- validFrom: new Date().toISOString(),
41
- credentialSubject: { id: 'did:example:subject-123' },
42
- id: 'urn:uuid:original-123',
43
- } as any);
44
-
45
- const makeOriginalWithoutIdButSubject = (): VC => ({
46
- '@context': [VC_V2],
47
- type: ['VerifiableCredential'],
48
- issuer: 'did:example:someone',
49
- validFrom: new Date().toISOString(),
50
- credentialSubject: { id: 'did:example:subject-abc' },
51
- } as any);
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
- test('endorseCredential: minimal (comment only) with original id', async () => {
57
- const lc = createMockLearnCard();
58
- const plugin = getLinkedClaimsPlugin(lc);
59
-
60
- const original = makeOriginalWithId();
61
-
62
- const endorsement = await plugin.methods.endorseCredential(
63
- lc,
64
- original,
65
- { endorsementComment: 'Solid work' },
66
- {}
67
- );
68
-
69
- const ctx = endorsement['@context'] as any[];
70
- expect(Array.isArray(ctx)).toBe(true);
71
- expect(ctx).toEqual(expect.arrayContaining([VC_V2, OBV3]));
72
-
73
- const types = endorsement.type as string[];
74
- expect(types).toEqual(expect.arrayContaining(['VerifiableCredential', 'EndorsementCredential']));
75
-
76
- expect((endorsement as any).issuer).toBe('did:example:issuer');
77
-
78
- const cs = (endorsement as any).credentialSubject;
79
- const csObj = Array.isArray(cs) ? cs[0] : cs;
80
- expect(csObj.id).toBe('urn:uuid:original-123'); // prefers original.id when present
81
- expect(csObj.type).toEqual(expect.arrayContaining(['EndorsementSubject']));
82
- expect(csObj.endorsementComment).toBe('Solid work');
83
-
84
- // default name applied when not provided
85
- expect((endorsement as any).name).toBe('Endorsement of urn:uuid:original-123');
86
- });
87
-
88
- test('endorseCredential: with name and description only, resolves subject id fallback', async () => {
89
- const lc = createMockLearnCard();
90
- const plugin = getLinkedClaimsPlugin(lc);
91
- const original = makeOriginalWithoutIdButSubject();
92
-
93
- const endorsement = await plugin.methods.endorseCredential(
94
- lc,
95
- original,
96
- { name: 'Kudos', description: 'Peer endorsement' },
97
- {}
98
- );
99
-
100
- const cs = (endorsement as any).credentialSubject;
101
- const csObj = Array.isArray(cs) ? cs[0] : cs;
102
- expect(csObj.id).toBe('did:example:subject-abc');
103
- expect((endorsement as any).name).toBe('Kudos');
104
- expect((endorsement as any).description).toBe('Peer endorsement');
105
- expect(csObj.endorsementComment).toBeUndefined();
106
- });
107
-
108
- test('endorseCredential: throws if original lacks id and credentialSubject.id', async () => {
109
- const lc = createMockLearnCard();
110
- const plugin = getLinkedClaimsPlugin(lc);
111
-
112
- const original = {
113
- '@context': [VC_V2],
114
- type: ['VerifiableCredential'],
115
- issuer: 'did:example:none',
116
- validFrom: new Date().toISOString(),
117
- credentialSubject: {},
118
- } as any as VC;
119
-
120
- await expect(
121
- plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {})
122
- ).rejects.toThrow('Original credential must have either id or credentialSubject.id');
123
- });
124
-
125
- test('verifyEndorsement: valid endorsement passes and adds linked-claims check', async () => {
126
- const lc = createMockLearnCard();
127
- const plugin = getLinkedClaimsPlugin(lc);
128
- const original = makeOriginalWithId();
129
-
130
- const endorsement = await plugin.methods.endorseCredential(lc, original, { endorsementComment: 'Nice' }, {});
131
-
132
- const result = await plugin.methods.verifyEndorsement(lc, endorsement, {});
133
- expect(result.errors).toHaveLength(0);
134
- expect(result.checks).toEqual(expect.arrayContaining(['linked-claims']));
135
- });
136
-
137
- test('verifyEndorsement: missing OBv3 context -> error', async () => {
138
- const lc = createMockLearnCard();
139
- const plugin = getLinkedClaimsPlugin(lc);
140
- const original = makeOriginalWithId();
141
-
142
- const endorsement = await plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {});
143
- const bad = clone(endorsement);
144
- (bad as any)['@context'] = [VC_V2];
145
-
146
- const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
147
- expect(res.errors.join(' ')).toMatch(/missing VCv2 or OBv3 context/);
148
- });
149
-
150
- test('verifyEndorsement: missing VC v2 context -> error', async () => {
151
- const lc = createMockLearnCard();
152
- const plugin = getLinkedClaimsPlugin(lc);
153
- const original = makeOriginalWithId();
154
-
155
- const endorsement = await plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {});
156
- const bad = clone(endorsement);
157
- (bad as any)['@context'] = [OBV3];
158
-
159
- const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
160
- expect(res.errors.join(' ')).toMatch(/missing VCv2 or OBv3 context/);
161
- });
162
-
163
- test('verifyEndorsement: missing EndorsementCredential type -> error', async () => {
164
- const lc = createMockLearnCard();
165
- const plugin = getLinkedClaimsPlugin(lc);
166
- const original = makeOriginalWithId();
167
-
168
- const endorsement = await plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {});
169
- const bad = clone(endorsement);
170
- (bad as any).type = ['VerifiableCredential'];
171
-
172
- const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
173
- expect(res.errors.join(' ')).toMatch(/missing EndorsementCredential type/);
174
- });
175
-
176
- test('verifyEndorsement: subject type missing EndorsementSubject -> error', async () => {
177
- const lc = createMockLearnCard();
178
- const plugin = getLinkedClaimsPlugin(lc);
179
- const original = makeOriginalWithId();
180
-
181
- const endorsement = await plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {});
182
- const bad = clone(endorsement);
183
- const cs = (bad as any).credentialSubject;
184
- const csObj = Array.isArray(cs) ? cs[0] : cs;
185
- csObj.type = ['SomeOtherType'];
186
-
187
- const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
188
- expect(res.errors.join(' ')).toMatch(/credentialSubject\.type must include EndorsementSubject/);
189
- });
190
-
191
- test('verifyEndorsement: missing credentialSubject.id -> error', async () => {
192
- const lc = createMockLearnCard();
193
- const plugin = getLinkedClaimsPlugin(lc);
194
- const original = makeOriginalWithId();
195
-
196
- const endorsement = await plugin.methods.endorseCredential(lc, original, { endorsementComment: 'x' }, {});
197
- const bad = clone(endorsement);
198
- const cs = (bad as any).credentialSubject;
199
- const csObj = Array.isArray(cs) ? cs[0] : cs;
200
- delete csObj.id;
201
-
202
- const res = await plugin.methods.verifyEndorsement(lc, bad as any, {});
203
- expect(res.errors.join(' ')).toMatch(/credentialSubject\.id missing/);
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
+ });