Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/slimy-tables-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@learncard/cli": patch
"@learncard/linked-claims-plugin": patch
---

Feat: LinkedClaims Plugin - Endorsements
1 change: 1 addition & 0 deletions packages/learn-card-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@learncard/didkit-plugin": "workspace:^",
"@learncard/init": "workspace:^",
"@learncard/learn-cloud-plugin": "workspace:*",
"@learncard/linked-claims-plugin": "workspace:*",
"@learncard/open-badge-v2-plugin": "workspace:*",
"@learncard/simple-signing-plugin": "workspace:*",
"@learncard/types": "workspace:*",
Expand Down
9 changes: 6 additions & 3 deletions packages/learn-card-cli/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { initLearnCard, emptyLearnCard, learnCardFromSeed } from '@learncard/ini
import { getSimpleSigningPlugin } from '@learncard/simple-signing-plugin';
import { openBadgeV2Plugin } from '@learncard/open-badge-v2-plugin';
import types from '@learncard/types';
import { getLinkedClaimsPlugin } from '@learncard/linked-claims-plugin';
import gradient from 'gradient-string';
import figlet from 'figlet';
import { program } from 'commander';
Expand Down Expand Up @@ -79,6 +80,11 @@ program
await getSimpleSigningPlugin(_learnCard, 'https://api.learncard.app/trpc')
);

// Add LinkedClaims plugin so endorse/verify/store/getEndorsements are available in the CLI
globalThis.learnCard = await globalThis.learnCard.addPlugin(
getLinkedClaimsPlugin(globalThis.learnCard)
);

// Add OpenBadge v2 wrapper plugin for backwards-compatible OBv2 -> VC wrapping
globalThis.learnCard = await globalThis.learnCard.addPlugin(
openBadgeV2Plugin(globalThis.learnCard)
Expand Down Expand Up @@ -139,9 +145,6 @@ program
console.log(
`β”‚ Verify a signed VP β”‚ await ${g.learnCard}.invoke.verifyPresentation(vp); β”‚`
);
console.log(
`β”‚ Wrap OpenBadge v2 JSON β”‚ await ${g.learnCard}.invoke.wrapOpenBadgeV2(urlOrObj); β”‚`
);
console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜');

console.log('');
Expand Down
3 changes: 3 additions & 0 deletions packages/plugins/linked-claims/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
tsconfig.tsbuildinfo
47 changes: 47 additions & 0 deletions packages/plugins/linked-claims/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# LinkedClaims Plugin

Create, verify, store, and retrieve endorsement credentials using Open Badges v3 EndorsementCredential, linked to original credentials by subject id or original credential id.

## Install
Workspace package. Build with Nx:

```
pnpm nx run linked-claims-plugin:build
```

## Usage

```ts
import { getLinkedClaimsPlugin } from '@learncard/linked-claims-plugin';

const lc = await LearnCard.fromSomewhere().then(lc => lc.addPlugin(getLinkedClaimsPlugin(lc)));

const endorsement = await lc.invoke.endorseCredential(originalVc, {
// OBv3 fields
endorsementComment: 'I endorse this credential.',
name: `Endorsement of ${originalVc.id ?? originalVc.credentialSubject?.id}`,
description: 'Peer endorsement',
});

const verified = await lc.invoke.verifyEndorsement(endorsement);

const { uri } = await lc.invoke.storeEndorsement(endorsement);

const endorsements = await lc.invoke.getEndorsements(originalVc);
```

## Endorsement Context
Uses VC v2 and OBv3 contexts:

```
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
]
```

### Endorsement Fields (details input)

- `endorsementComment?: string`
- `name?: string`
- `description?: string`
23 changes: 23 additions & 0 deletions packages/plugins/linked-claims/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/src/test/**/*.test.ts'],
transform: {
'^.+\\.(t|j)sx?$': [
'ts-jest',
{
tsconfig: {
target: 'ES2020',
module: 'CommonJS',
moduleResolution: 'node',
esModuleInterop: true,
isolatedModules: true,
skipLibCheck: true,
allowSyntheticDefaultImports: true,
},
diagnostics: false
}
]
},
transformIgnorePatterns: ['/node_modules/'],
};
44 changes: 44 additions & 0 deletions packages/plugins/linked-claims/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@learncard/linked-claims-plugin",
"version": "0.1.0",
"description": "LinkedClaims plugin to create, verify, store, and query endorsement credentials.",
"main": "./dist/index.js",
"module": "./dist/linked-claims-plugin.esm.js",
"files": [
"dist"
],
"scripts": {
"build": "node ./scripts/build.mjs && shx cp ./scripts/mixedEntypoint.js ./dist/index.js && tsc --p tsconfig.json",
"test": "jest --passWithNoTests",
"test:watch": "jest --watch",
"test:coverage": "jest --silent --ci --coverage --coverageReporters=\"text\" --coverageReporters=\"text-summary\""
},
"author": "Learning Economy Foundation (www.learningeconomy.io)",
"license": "MIT",
"homepage": "https://github.com/WeLibraryOS/LearnCard#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/WeLibraryOS/LearnCard.git"
},
"bugs": {
"url": "https://github.com/WeLibraryOS/LearnCard/issues"
},
"devDependencies": {
"@types/jest": "^29.2.2",
"@types/node": "^17.0.31",
"aqu": "0.4.3",
"esbuild": "^0.14.38",
"esbuild-jest": "^0.5.0",
"esbuild-plugin-copy": "^1.3.0",
"jest": "^29.3.0",
"shx": "^0.3.4",
"ts-jest": "^29.0.3"
},
"types": "./dist/index.d.ts",
"dependencies": {
"@learncard/core": "workspace:*",
"@learncard/types": "workspace:*",
"@learncard/vc-plugin": "workspace:^",
"why-is-node-running": "^2.2.2"
}
}
20 changes: 20 additions & 0 deletions packages/plugins/linked-claims/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"name": "linked-claims-plugin",
"sourceRoot": "packages/plugins/linked-claims/src",
"projectType": "library",
"root": "packages/plugins/linked-claims",
"tags": [],
"implicitDependencies": ["core", "vc-plugin"],
"namedInputs": {
"scripts": ["{projectRoot}/scripts/**/*"],
"source": ["{projectRoot}/src/**/*"]
},
"targets": {
"build": {
"executor": "nx:run-script",
"inputs": ["source", "scripts"],
"options": { "script": "build" }
}
}
}
79 changes: 79 additions & 0 deletions packages/plugins/linked-claims/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import path from 'path';

import esbuild from 'esbuild';
import rimraf from 'rimraf';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐞 Bug - Missing Dependency: Add 'rimraf' as a devDependency in the package.json file, or replace the rimraf usage with Node.js built-in fs.rmSync() for a dependency-free solution.

Suggested change
import rimraf from 'rimraf';
import fs from 'fs';


const buildOptions = {
target: 'es2020',
sourcemap: true,
external: ['isomorphic-fetch', 'isomorphic-webcrypto'],
};

const configurations = [
{
keepNames: true,
bundle: true,
sourcemap: 'external',
incremental: true,
tsconfig: 'tsconfig.json',
plugins: [],
entryPoints: ['src/index.ts'],
format: 'cjs',
outfile: 'dist/linked-claims-plugin.cjs.development.js',
...buildOptions,
},
{
keepNames: true,
bundle: true,
sourcemap: 'external',
incremental: true,
tsconfig: 'tsconfig.json',
plugins: [],
entryPoints: ['src/index.ts'],
minify: true,
format: 'cjs',
outfile: 'dist/linked-claims-plugin.cjs.production.min.js',
...buildOptions,
},
{
keepNames: true,
bundle: true,
sourcemap: 'external',
incremental: true,
tsconfig: 'tsconfig.json',
plugins: [],
entryPoints: ['src/index.ts'],
format: 'esm',
outfile: 'dist/linked-claims-plugin.esm.js',
...buildOptions,
},
];

function asyncRimraf(path) {
return new Promise((resolve, reject) => {
rimraf(path, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

await Promise.all(
configurations.map(async config => {
var dir = config.outdir || path.dirname(config.outfile);
await asyncRimraf(dir).catch(() => {
console.log('Unable to delete directory', dir);
});
})
);

await Promise.all(configurations.map(config => esbuild.build(config))).catch(err => {
console.error('❌ Build failed');
process.exit(1);
});

console.log('βœ” Build successful');
process.exit(0);
7 changes: 7 additions & 0 deletions packages/plugins/linked-claims/scripts/mixedEntypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./linked-claims-plugin.cjs.production.min.js');
} else {
module.exports = require('./linked-claims-plugin.cjs.development.js');
}
Loading
Loading