@learncard/linked-claims-plugin 0.2.24 → 0.2.25

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
@@ -3,10 +3,11 @@
3
3
  Create, verify, store, and retrieve endorsement credentials using Open Badges v3 EndorsementCredential, linked to original credentials by subject id or original credential id.
4
4
 
5
5
  ## Install
6
+
6
7
  Workspace package. Build with Nx:
7
8
 
8
9
  ```
9
- pnpm nx run linked-claims-plugin:build
10
+ bunx nx run linked-claims-plugin:build
10
11
  ```
11
12
 
12
13
  ## Usage
@@ -17,10 +18,10 @@ import { getLinkedClaimsPlugin } from '@learncard/linked-claims-plugin';
17
18
  const lc = await LearnCard.fromSomewhere().then(lc => lc.addPlugin(getLinkedClaimsPlugin(lc)));
18
19
 
19
20
  const endorsement = await lc.invoke.endorseCredential(originalVc, {
20
- // OBv3 fields
21
- endorsementComment: 'I endorse this credential.',
22
- name: `Endorsement of ${originalVc.id ?? originalVc.credentialSubject?.id}`,
23
- description: 'Peer endorsement',
21
+ // OBv3 fields
22
+ endorsementComment: 'I endorse this credential.',
23
+ name: `Endorsement of ${originalVc.id ?? originalVc.credentialSubject?.id}`,
24
+ description: 'Peer endorsement',
24
25
  });
25
26
 
26
27
  const verified = await lc.invoke.verifyEndorsement(endorsement);
@@ -31,6 +32,7 @@ const endorsements = await lc.invoke.getEndorsements(originalVc);
31
32
  ```
32
33
 
33
34
  ## Endorsement Context
35
+
34
36
  Uses VC v2 and OBv3 contexts:
35
37
 
36
38
  ```
@@ -42,6 +44,6 @@ Uses VC v2 and OBv3 contexts:
42
44
 
43
45
  ### Endorsement Fields (details input)
44
46
 
45
- - `endorsementComment?: string`
46
- - `name?: string`
47
- - `description?: string`
47
+ - `endorsementComment?: string`
48
+ - `name?: string`
49
+ - `description?: string`
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
1
  {
2
- "name": "@learncard/linked-claims-plugin",
3
- "version": "0.2.24",
4
- "description": "LinkedClaims plugin to create, verify, store, and query endorsement credentials.",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/linked-claims-plugin.esm.js",
8
- "exports": {
9
- ".": {
10
- "import": {
11
- "types": "./dist/index.d.ts",
12
- "default": "./dist/linked-claims-plugin.esm.js"
13
- },
14
- "require": {
15
- "types": "./dist/index.d.cts",
16
- "default": "./dist/index.cjs"
17
- }
18
- }
19
- },
20
- "files": [
21
- "dist"
22
- ],
23
- "author": "Learning Economy Foundation (www.learningeconomy.io)",
24
- "license": "MIT",
25
- "homepage": "https://github.com/learningeconomy/LearnCard/tree/main/packages/plugins/linked-claims/README.md",
26
- "repository": {
27
- "type": "git",
28
- "url": "https://github.com/learningeconomy/LearnCard"
29
- },
30
- "bugs": {
31
- "url": "https://github.com/learningeconomy/LearnCard/issues"
32
- },
33
- "devDependencies": {
34
- "@types/jest": "^29.2.2",
35
- "@types/node": "^17.0.31",
36
- "aqu": "0.4.3",
37
- "esbuild": "^0.27.1",
38
- "esbuild-jest": "^0.5.0",
39
- "esbuild-plugin-copy": "^1.3.0",
40
- "jest": "^29.3.0",
41
- "shx": "^0.3.4",
42
- "ts-jest": "^29.0.3"
43
- },
44
- "types": "./dist/index.d.ts",
45
- "dependencies": {
46
- "why-is-node-running": "^2.2.2",
47
- "@learncard/core": "9.4.24",
48
- "@learncard/types": "5.17.4",
49
- "@learncard/vc-plugin": "^1.5.4"
50
- },
51
- "sideEffects": false,
52
- "scripts": {
53
- "build": "node ./scripts/build.mjs && shx cp ./scripts/mixedEntypoint.js ./dist/index.cjs && tsc --p tsconfig.json && shx cp ./dist/index.d.ts ./dist/index.d.cts",
54
- "test": "jest --passWithNoTests",
55
- "test:watch": "jest --watch",
56
- "test:coverage": "jest --silent --ci --coverage --coverageReporters=\"text\" --coverageReporters=\"text-summary\""
57
- }
58
- }
2
+ "name": "@learncard/linked-claims-plugin",
3
+ "version": "0.2.25",
4
+ "description": "LinkedClaims plugin to create, verify, store, and query endorsement credentials.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/linked-claims-plugin.esm.js",
8
+ "exports": {
9
+ ".": {
10
+ "development": "./src/index.ts",
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/linked-claims-plugin.esm.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "scripts": {
26
+ "build": "node ./scripts/build.mjs && shx cp ./scripts/mixedEntypoint.js ./dist/index.cjs && tsc --p tsconfig.json && shx cp ./dist/index.d.ts ./dist/index.d.cts",
27
+ "test": "jest --passWithNoTests",
28
+ "test:watch": "jest --watch",
29
+ "test:coverage": "jest --silent --ci --coverage --coverageReporters=\"text\" --coverageReporters=\"text-summary\""
30
+ },
31
+ "author": "Learning Economy Foundation (www.learningeconomy.io)",
32
+ "license": "MIT",
33
+ "homepage": "https://github.com/learningeconomy/LearnCard/tree/main/packages/plugins/linked-claims/README.md",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/learningeconomy/LearnCard"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/learningeconomy/LearnCard/issues"
40
+ },
41
+ "devDependencies": {
42
+ "@types/jest": "^29.2.2",
43
+ "@types/node": "^17.0.31",
44
+ "aqu": "0.4.3",
45
+ "esbuild": "^0.27.1",
46
+ "esbuild-jest": "^0.5.0",
47
+ "esbuild-plugin-copy": "^1.3.0",
48
+ "jest": "^29.3.0",
49
+ "shx": "^0.3.4",
50
+ "ts-jest": "^29.0.3"
51
+ },
52
+ "types": "./dist/index.d.ts",
53
+ "dependencies": {
54
+ "@learncard/core": "workspace:*",
55
+ "@learncard/types": "workspace:*",
56
+ "@learncard/vc-plugin": "workspace:^",
57
+ "why-is-node-running": "^2.2.2"
58
+ },
59
+ "sideEffects": false
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,180 @@
1
+ import type { LearnCard } from '@learncard/core';
2
+ import type { VC, UnsignedVC } from '@learncard/types';
3
+ import type { LinkedClaimsPlugin, LinkedClaimsPluginDependentMethods } from './types';
4
+
5
+ export * from './types';
6
+
7
+ const ENDORSEMENT_CONTEXT = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';
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
+ const pickIndexProvider = (learnCard: any, preferred?: string): string | undefined => {
16
+ const providers = Object.keys(learnCard.index || {}).filter(
17
+ k => !['providers', 'all'].includes(k)
18
+ );
19
+ if (!providers.length) return undefined;
20
+ if (preferred && providers.includes(preferred)) return preferred;
21
+ if (providers.includes('LearnCloud')) return 'LearnCloud';
22
+ return providers[0];
23
+ };
24
+
25
+ const pickStoreProvider = (learnCard: any, preferred?: string): string | undefined => {
26
+ const providers = Object.keys(learnCard.store || {}).filter(k => k !== 'providers');
27
+ if (!providers.length) return undefined;
28
+ if (preferred && providers.includes(preferred)) return preferred;
29
+ return providers[0];
30
+ };
31
+
32
+ const ensureArray = <T>(arr: T | T[] | undefined): T[] =>
33
+ Array.isArray(arr) ? arr : arr ? [arr] : [];
34
+
35
+ export const getLinkedClaimsPlugin = (
36
+ learnCard: LearnCard<any, 'id' | 'store' | 'index' | 'read', LinkedClaimsPluginDependentMethods>
37
+ ): LinkedClaimsPlugin => ({
38
+ name: 'LinkedClaims',
39
+ displayName: 'LinkedClaims',
40
+ description: 'Create, verify, store, and retrieve endorsement credentials linked to originals.',
41
+ methods: {
42
+ endorseCredential: async (_learnCard, original, details, opts) => {
43
+ const issuer = learnCard.id.did();
44
+
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');
50
+
51
+ const context: (string | Record<string, any>)[] = [
52
+ 'https://www.w3.org/ns/credentials/v2',
53
+ ENDORSEMENT_CONTEXT,
54
+ ];
55
+
56
+ const types: [string, ...string[]] = ['VerifiableCredential', 'EndorsementCredential'];
57
+
58
+ const unsigned: UnsignedVC = {
59
+ '@context': context as any,
60
+ type: types,
61
+ issuer,
62
+ validFrom: new Date().toISOString(),
63
+ credentialSubject: {
64
+ type: ['EndorsementSubject'],
65
+ id: targetId,
66
+ ...(details.endorsementComment
67
+ ? { endorsementComment: details.endorsementComment }
68
+ : {}),
69
+ },
70
+ ...(details.name ? { name: details.name } : { name: `Endorsement of ${targetId}` }),
71
+ ...(details.description ? { description: details.description } : {}),
72
+ ...(details.evidence && { evidence: details.evidence as any }),
73
+ };
74
+
75
+ return learnCard.invoke.issueCredential(unsigned);
76
+ },
77
+
78
+ verifyEndorsement: async (_learnCard, endorsement, options) => {
79
+ const check = await learnCard.invoke.verifyCredential(endorsement, options);
80
+
81
+ const ctx = ensureArray((endorsement as any)['@context']);
82
+ const types = ensureArray((endorsement as any)['type']);
83
+
84
+ if (
85
+ !ctx.some((c: string) => c === 'https://www.w3.org/ns/credentials/v2') ||
86
+ !ctx.some((c: string) => c === ENDORSEMENT_CONTEXT)
87
+ ) {
88
+ check.errors.push('linked-claims error: missing VCv2 or OBv3 context');
89
+ }
90
+ if (!types.includes('EndorsementCredential')) {
91
+ check.errors.push('linked-claims error: missing EndorsementCredential type');
92
+ }
93
+
94
+ const cs: any = (endorsement as any).credentialSubject;
95
+ const csObj = Array.isArray(cs) ? cs[0] : cs;
96
+ const csTypes = ensureArray<string>(csObj?.type);
97
+ if (!csTypes.includes('EndorsementSubject')) {
98
+ check.errors.push(
99
+ 'linked-claims error: credentialSubject.type must include EndorsementSubject'
100
+ );
101
+ }
102
+ if (!csObj?.id) {
103
+ check.errors.push('linked-claims error: credentialSubject.id missing');
104
+ }
105
+
106
+ if (!check.errors.length) check.checks.push('linked-claims');
107
+ return check;
108
+ },
109
+
110
+ storeEndorsement: async (_learnCard, endorsement, options) => {
111
+ const storeName = pickStoreProvider(learnCard, options?.storeName);
112
+ if (!storeName) throw new Error('No store plane provider available');
113
+ if (
114
+ !learnCard.store[storeName].uploadEncrypted ||
115
+ typeof learnCard.store[storeName].uploadEncrypted !== 'function'
116
+ ) {
117
+ throw new Error(
118
+ `Store provider '${storeName}' does not support uploadEncrypted method`
119
+ );
120
+ }
121
+ const uri: string = await (learnCard.store as any)[storeName].uploadEncrypted(
122
+ endorsement
123
+ );
124
+
125
+ const indexName = pickIndexProvider(learnCard, options?.indexName);
126
+ let indexed = false;
127
+ let id =
128
+ (endorsement as any).id ||
129
+ `urn:uuid:${(globalThis as any).crypto?.randomUUID?.() ||
130
+ (() => {
131
+ throw new Error(
132
+ 'Secure random UUID generation is not available. Please use an environment that supports crypto.randomUUID().'
133
+ );
134
+ })()
135
+ }`;
136
+
137
+ if (indexName) {
138
+ const cs: any = (endorsement as any).credentialSubject;
139
+ const csObj = Array.isArray(cs) ? cs[0] : cs;
140
+ const record = {
141
+ id,
142
+ uri,
143
+ type: ensureArray((endorsement as any).type),
144
+ endorsedId: csObj?.id,
145
+ subjectId: csObj?.id,
146
+ originalCredentialId: csObj?.id,
147
+ issuedOn: (endorsement as any).validFrom || (endorsement as any).issuanceDate,
148
+ category: 'Endorsement',
149
+ credentialId: options?.credentialId, // original credential id
150
+ sharedUri: options?.sharedUri, // original credential shared uri
151
+ relationship: options?.relationship,
152
+ visibility: options?.visibility ?? 'public',
153
+ };
154
+ indexed = await (learnCard.index as any)[indexName].add(record);
155
+ }
156
+
157
+ return { uri, indexed, id, indexName, storeName };
158
+ },
159
+
160
+ getEndorsements: async (_learnCard, original, options) => {
161
+ const indexName = pickIndexProvider(learnCard, options?.indexName);
162
+ if (!indexName) return [];
163
+
164
+ const subjectIds = getSubjectIds(original);
165
+ const originalId = (original as any).id;
166
+
167
+ const query = originalId
168
+ ? { originalCredentialId: originalId }
169
+ : { endorsedId: subjectIds[0] };
170
+ const records = await (learnCard.index as any)[indexName].get(query);
171
+
172
+ const results: VC[] = [];
173
+ for (const rec of records) {
174
+ const vc = await learnCard.read.get(rec.uri);
175
+ if (vc) results.push(vc as VC);
176
+ }
177
+ return results;
178
+ },
179
+ },
180
+ });
@@ -0,0 +1,205 @@
1
+ import { getLinkedClaimsPlugin } from '../index';
2
+ import type { VC, VerificationCheck } from '@learncard/types';
3
+
4
+ // Minimal mock LearnCard with just the surfaces the plugin uses
5
+ 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;
31
+ };
32
+
33
+ const VC_V2 = 'https://www.w3.org/ns/credentials/v2';
34
+ const OBV3 = 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json';
35
+
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);
52
+
53
+ const clone = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
54
+
55
+ 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
+ });
205
+ });
package/src/types.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { Plugin } from '@learncard/core';
2
+ import { VC, VerificationCheck } from '@learncard/types';
3
+ import { VCPluginMethods } from '@learncard/vc-plugin';
4
+ import { ProofOptions } from '@learncard/didkit-plugin';
5
+
6
+ export type EndorsementEvidence = {
7
+ id?: string;
8
+ type: string | [string, ...string[]]; // Changed from string[] to match VC2Evidence
9
+ name?: string;
10
+ narrative?: string;
11
+ description?: string;
12
+ genre?: string;
13
+ audience?: string;
14
+ [key: string]: any; // Allow additional properties
15
+ };
16
+
17
+ export type EndorsementDetails = {
18
+ // OBv3 Endorsement fields
19
+ endorsementComment?: string;
20
+ name?: string;
21
+ description?: string;
22
+ evidence?: EndorsementEvidence;
23
+ };
24
+
25
+ export type EndorsementRelationship = {
26
+ type: string;
27
+ label: string;
28
+ };
29
+
30
+ export type EndorsementVisibility = 'public' | 'private';
31
+
32
+ export type StoreIndexOptions = {
33
+ storeName?: string;
34
+ indexName?: string;
35
+ credentialId?: string;
36
+ sharedUri?: string;
37
+ relationship?: EndorsementRelationship;
38
+ visibility?: EndorsementVisibility;
39
+ };
40
+
41
+ export type LinkedClaimsPluginDependentMethods = VCPluginMethods;
42
+
43
+ export type LinkedClaimsPluginMethods = {
44
+ endorseCredential: (
45
+ original: VC,
46
+ details: EndorsementDetails,
47
+ options?: { contextUrl?: string }
48
+ ) => Promise<VC>;
49
+
50
+ verifyEndorsement: (
51
+ endorsement: VC,
52
+ options?: Partial<ProofOptions>
53
+ ) => Promise<VerificationCheck>;
54
+
55
+ storeEndorsement: (
56
+ endorsement: VC,
57
+ options?: StoreIndexOptions
58
+ ) => Promise<{
59
+ uri: string;
60
+ indexed: boolean;
61
+ id: string;
62
+ indexName?: string;
63
+ storeName?: string;
64
+ }>;
65
+
66
+ getEndorsements: (original: VC, options?: { indexName?: string }) => Promise<VC[]>;
67
+ };
68
+
69
+ export type LinkedClaimsPlugin = Plugin<
70
+ 'LinkedClaims',
71
+ any,
72
+ LinkedClaimsPluginMethods,
73
+ 'id' | 'store' | 'index' | 'read',
74
+ LinkedClaimsPluginDependentMethods
75
+ >;
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Learning Economy Foundation <sdk@learningeconomy.io>
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.