@learncard/open-badge-v2-plugin 1.1.24 → 1.1.26

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
@@ -2,38 +2,41 @@
2
2
 
3
3
  Wrap legacy Open Badges v2.0 (OBv2) assertions into self‑issued W3C Verifiable Credentials (VCs) so they can be stored, indexed, and used within the LearnCard ecosystem.
4
4
 
5
- - Name: `OpenBadgeV2`
6
- - Method: `wrapOpenBadgeV2(obv2Assertion: object | string): Promise<VC>`
7
- - Context added: `https://ctx.learncard.com/wrappers/obv2/1.0.0.json`
8
- - VC type produced: `LegacyOpenBadgeCredential`
5
+ - Name: `OpenBadgeV2`
6
+ - Method: `wrapOpenBadgeV2(obv2Assertion: object | string): Promise<VC>`
7
+ - Context added: `https://ctx.learncard.com/wrappers/obv2/1.0.0.json`
8
+ - VC type produced: `LegacyOpenBadgeCredential`
9
9
 
10
10
  ## Why this plugin?
11
11
 
12
12
  Many issuers still publish Open Badges v2.0 assertions (JSON), not VCs. This plugin provides a backwards‑compatibility bridge by wrapping the raw OBv2 assertion inside a signed VC you control, preserving the original badge payload under `legacyAssertion`.
13
13
 
14
14
  This enables:
15
- - Use of legacy badges anywhere a VC is expected (storage, indexing, sharing).
16
- - A stable LearnCard vocabulary for the wrapper (`LegacyOpenBadgeCredential`).
17
- - Consistent DID‑based signing and verification workflows.
15
+
16
+ - Use of legacy badges anywhere a VC is expected (storage, indexing, sharing).
17
+ - A stable LearnCard vocabulary for the wrapper (`LegacyOpenBadgeCredential`).
18
+ - Consistent DID‑based signing and verification workflows.
18
19
 
19
20
  ## What it produces
20
21
 
21
22
  A signed VC with these characteristics:
22
- - `@context`: `["https://www.w3.org/ns/credentials/v2", "https://ctx.learncard.com/wrappers/obv2/1.0.0.json"]`
23
- - `type`: `["VerifiableCredential", "LegacyOpenBadgeCredential"]`
24
- - `issuer`: your LearnCard DID
25
- - `validFrom`: `new Date().toISOString()`
26
- - `credentialSubject.id`: your LearnCard DID
27
- - `legacyAssertion`: the original OBv2 assertion JSON
23
+
24
+ - `@context`: `["https://www.w3.org/ns/credentials/v2", "https://ctx.learncard.com/wrappers/obv2/1.0.0.json"]`
25
+ - `type`: `["VerifiableCredential", "LegacyOpenBadgeCredential"]`
26
+ - `issuer`: your LearnCard DID
27
+ - `validFrom`: `new Date().toISOString()`
28
+ - `credentialSubject.id`: your LearnCard DID
29
+ - `legacyAssertion`: the original OBv2 assertion JSON
28
30
 
29
31
  Notes:
30
- - The wrapper signs the fact that “you attest to this legacy assertion,” not that you were the original OBv2 issuer.
31
- - Minimal OBv2 validation is performed: `id` (string), `type` includes `"Assertion"`, `issuedOn` (string).
32
+
33
+ - The wrapper signs the fact that “you attest to this legacy assertion,” not that you were the original OBv2 issuer.
34
+ - Minimal OBv2 validation is performed: `id` (string), `type` includes `"Assertion"`, `issuedOn` (string).
32
35
 
33
36
  ## Requirements
34
37
 
35
- - A LearnCard instance with the VC plugin installed (used to sign the VC).
36
- - Fetch available for remote inputs (Node 18+ or a polyfill like `undici`).
38
+ - A LearnCard instance with the VC plugin installed (used to sign the VC).
39
+ - Fetch available for remote inputs (Node 18+ or a polyfill like `undici`).
37
40
 
38
41
  ## Installation
39
42
 
@@ -41,7 +44,7 @@ This package is part of the monorepo. In external usage, install from npm (when
41
44
 
42
45
  ```bash
43
46
  # inside the monorepo
44
- pnpm nx run open-badge-v2-plugin:build
47
+ bunx nx run open-badge-v2-plugin:build
45
48
  ```
46
49
 
47
50
  ## Usage
@@ -80,13 +83,13 @@ const vc = await learnCard.invoke.wrapOpenBadgeV2(ipfsUrl);
80
83
 
81
84
  ```ts
82
85
  const obv2 = {
83
- '@context': 'https://w3id.org/openbadges/v2',
84
- type: 'Assertion',
85
- id: 'https://issuer.example.org/assertions/abc',
86
- issuedOn: '2024-05-02T12:34:56Z',
87
- recipient: { type: 'email', identity: 'user@example.org', hashed: false },
88
- badge: 'https://issuer.example.org/badges/my-badge'
89
- // ... any other OBv2 fields ...
86
+ '@context': 'https://w3id.org/openbadges/v2',
87
+ type: 'Assertion',
88
+ id: 'https://issuer.example.org/assertions/abc',
89
+ issuedOn: '2024-05-02T12:34:56Z',
90
+ recipient: { type: 'email', identity: 'user@example.org', hashed: false },
91
+ badge: 'https://issuer.example.org/badges/my-badge',
92
+ // ... any other OBv2 fields ...
90
93
  };
91
94
 
92
95
  const vc = await learnCard.invoke.wrapOpenBadgeV2(obv2);
@@ -105,36 +108,38 @@ const vc = await learnCard.invoke.wrapOpenBadgeV2(obv2String);
105
108
  wrapOpenBadgeV2(obv2Assertion: object | string): Promise<VC>
106
109
  ```
107
110
 
108
- - `obv2Assertion` may be:
109
- - A raw OBv2 assertion object
110
- - A URL string (http(s)://, ipfs://, ipns://) pointing to the assertion JSON
111
- - A JSON string containing the assertion
111
+ - `obv2Assertion` may be:
112
+ - A raw OBv2 assertion object
113
+ - A URL string (http(s)://, ipfs://, ipns://) pointing to the assertion JSON
114
+ - A JSON string containing the assertion
112
115
 
113
116
  Validation rules enforced:
114
- - `assertion.id` must be a non‑empty string
115
- - `assertion.type` must include `"Assertion"`
116
- - `assertion.issuedOn` must be a string
117
+
118
+ - `assertion.id` must be a non‑empty string
119
+ - `assertion.type` must include `"Assertion"`
120
+ - `assertion.issuedOn` must be a string
117
121
 
118
122
  ## JSON‑LD Context
119
123
 
120
124
  The wrapper vocabulary is published at:
121
125
 
122
- - `https://ctx.learncard.com/wrappers/obv2/1.0.0.json`
126
+ - `https://ctx.learncard.com/wrappers/obv2/1.0.0.json`
123
127
 
124
128
  It defines:
125
- - `LegacyOpenBadgeCredential`
126
- - `legacyAssertion` as `@type: @json` for embedding raw OBv2 JSON
129
+
130
+ - `LegacyOpenBadgeCredential`
131
+ - `legacyAssertion` as `@type: @json` for embedding raw OBv2 JSON
127
132
 
128
133
  ## Development
129
134
 
130
- - Build: `pnpm nx run open-badge-v2-plugin:build`
131
- - Source: `packages/plugins/open-badge-v2/src/`
132
- - Context file: `packages/learn-card-contexts/wrappers/obv2/1.0.0.json`
135
+ - Build: `bunx nx run open-badge-v2-plugin:build`
136
+ - Source: `packages/plugins/open-badge-v2/src/`
137
+ - Context file: `packages/learn-card-contexts/wrappers/obv2/1.0.0.json`
133
138
 
134
139
  ## Caveats
135
140
 
136
- - Wrapping does not retroactively convert OBv2 into a first‑party VC issued by the original badge issuer; it produces a VC signed by your DID that embeds the legacy assertion.
137
- - Always fetch and wrap assertions from trusted sources.
141
+ - Wrapping does not retroactively convert OBv2 into a first‑party VC issued by the original badge issuer; it produces a VC signed by your DID that embeds the legacy assertion.
142
+ - Always fetch and wrap assertions from trusted sources.
138
143
 
139
144
  ## License
140
145
 
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
1
  {
2
- "name": "@learncard/open-badge-v2-plugin",
3
- "version": "1.1.24",
4
- "description": "OpenBadge v2 wrapper plugin for LearnCard. Wraps OBv2 assertions into self-issued VCs.",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/open-badge-v2-plugin.esm.js",
8
- "exports": {
9
- ".": {
10
- "import": {
11
- "types": "./dist/index.d.ts",
12
- "default": "./dist/open-badge-v2-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/open-badge-v2/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
- "@learncard/core": "9.4.23",
47
- "@learncard/types": "5.17.3",
48
- "@learncard/vc-plugin": "1.5.3",
49
- "@learncard/crypto-plugin": "1.1.23"
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/open-badge-v2-plugin",
3
+ "version": "1.1.26",
4
+ "description": "OpenBadge v2 wrapper plugin for LearnCard. Wraps OBv2 assertions into self-issued VCs.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/open-badge-v2-plugin.esm.js",
8
+ "exports": {
9
+ ".": {
10
+ "development": "./src/index.ts",
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/open-badge-v2-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/open-badge-v2/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/crypto-plugin": "workspace:*",
56
+ "@learncard/vc-plugin": "workspace:*",
57
+ "@learncard/types": "workspace:*"
58
+ },
59
+ "sideEffects": false
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { openBadgeV2Plugin } from './plugin';
2
+ export * from './types';
package/src/plugin.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { LearnCard } from '@learncard/core';
2
+ import { UnsignedVC, VC } from '@learncard/types';
3
+
4
+ import {
5
+ OpenBadgeV2Plugin,
6
+ OBV2_WRAPPER_CONTEXT_URL,
7
+ OpenBadgeV2PluginDependentMethods,
8
+ } from './types';
9
+
10
+ const VC_V2_CONTEXT = 'https://www.w3.org/ns/credentials/v2';
11
+
12
+ const isUrl = (value: string): boolean =>
13
+ /^(https?:)?\/\//i.test(value) || value.startsWith('ipfs://') || value.startsWith('ipns://');
14
+
15
+ const isValidAndSafeUrl = (url: string): void => {
16
+ try {
17
+ const parsedUrl = new URL(url);
18
+
19
+ // Only allow http, https, ipfs, ipns protocols
20
+ const allowedProtocols = ['https:', 'http:', 'ipfs:', 'ipns:'];
21
+ if (!allowedProtocols.includes(parsedUrl.protocol)) {
22
+ throw new Error(
23
+ `wrapOpenBadgeV2: Protocol '${parsedUrl.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`
24
+ );
25
+ }
26
+
27
+ // Skip internal network checks for IPFS/IPNS as they don't use traditional IP addresses
28
+ if (parsedUrl.protocol === 'ipfs:' || parsedUrl.protocol === 'ipns:') {
29
+ return;
30
+ }
31
+
32
+ // Block localhost and internal networks for HTTP(S)
33
+ const hostname = parsedUrl.hostname.toLowerCase();
34
+
35
+ // Block localhost variations
36
+ if (
37
+ hostname === 'localhost' ||
38
+ hostname === '127.0.0.1' ||
39
+ hostname === '0.0.0.0' ||
40
+ hostname === '::1'
41
+ ) {
42
+ throw new Error('wrapOpenBadgeV2: Access to localhost is not allowed');
43
+ }
44
+
45
+ // Block private IP ranges (RFC 1918)
46
+ if (
47
+ hostname.startsWith('10.') ||
48
+ hostname.startsWith('192.168.') ||
49
+ (hostname.startsWith('172.') && /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname))
50
+ ) {
51
+ throw new Error('wrapOpenBadgeV2: Access to private IP ranges is not allowed');
52
+ }
53
+
54
+ // Block link-local addresses
55
+ if (hostname.startsWith('169.254.')) {
56
+ throw new Error('wrapOpenBadgeV2: Access to link-local addresses is not allowed');
57
+ }
58
+ } catch (error) {
59
+ if (error instanceof TypeError) {
60
+ throw new Error('wrapOpenBadgeV2: Invalid URL format');
61
+ }
62
+ throw error;
63
+ }
64
+ };
65
+
66
+ async function getObv2Assertion(input: object | string): Promise<Record<string, any>> {
67
+ if (typeof input === 'string') {
68
+ // Treat string as URL. If it looks like raw JSON, try to parse for developer ergonomics
69
+ const trimmed = input.trim();
70
+ if (!isUrl(trimmed) && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
71
+ try {
72
+ return JSON.parse(trimmed);
73
+ } catch (e) {
74
+ throw new Error('wrapOpenBadgeV2: Provided string is not a valid URL or JSON');
75
+ }
76
+ }
77
+
78
+ // Validate URL for security
79
+ isValidAndSafeUrl(trimmed);
80
+
81
+ try {
82
+ // Add timeout to prevent hanging requests (10 seconds)
83
+ const controller = new AbortController();
84
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
85
+
86
+ const res = await fetch(trimmed, {
87
+ signal: controller.signal,
88
+ });
89
+
90
+ clearTimeout(timeoutId);
91
+
92
+ if (!res.ok)
93
+ throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${res.status}`);
94
+
95
+ return await res.json();
96
+ } catch (error: any) {
97
+ if (error instanceof TypeError) {
98
+ throw new Error(
99
+ `wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${error.message}`
100
+ );
101
+ }
102
+ if (error?.name === 'AbortError') {
103
+ throw new Error('wrapOpenBadgeV2: Request timed out after 10 seconds');
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ // Add basic type validation before assertion
110
+ if (typeof input !== 'object' || input === null) {
111
+ throw new Error('wrapOpenBadgeV2: Input must be a valid object');
112
+ }
113
+
114
+ return input as Record<string, any>;
115
+ }
116
+
117
+ function validateObv2(obv2: Record<string, any>): void {
118
+ if (!obv2 || typeof obv2 !== 'object') throw new Error('wrapOpenBadgeV2: Missing assertion');
119
+
120
+ const id = obv2.id;
121
+ if (typeof id !== 'string' || !id.length)
122
+ throw new Error('wrapOpenBadgeV2: assertion.id is required');
123
+
124
+ const t = obv2.type;
125
+ const hasAssertionType =
126
+ (typeof t === 'string' && t === 'Assertion') ||
127
+ (Array.isArray(t) && t.includes('Assertion'));
128
+ if (!hasAssertionType)
129
+ throw new Error("wrapOpenBadgeV2: assertion.type must include 'Assertion'");
130
+
131
+ if (!obv2.issuedOn || typeof obv2.issuedOn !== 'string') {
132
+ throw new Error('wrapOpenBadgeV2: assertion.issuedOn (string) is required');
133
+ }
134
+ }
135
+
136
+ export const openBadgeV2Plugin = (
137
+ learnCard: LearnCard<any, 'id', OpenBadgeV2PluginDependentMethods>
138
+ ): OpenBadgeV2Plugin => ({
139
+ name: 'OpenBadgeV2',
140
+ displayName: 'OpenBadge v2 Wrapper',
141
+ description: 'Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials',
142
+ methods: {
143
+ wrapOpenBadgeV2: async (_learnCard, obv2Assertion: object | string): Promise<VC> => {
144
+ const issuerDid = learnCard.id.did();
145
+
146
+ const obv2 = await getObv2Assertion(obv2Assertion);
147
+ validateObv2(obv2);
148
+
149
+ const unsigned: UnsignedVC = {
150
+ '@context': [VC_V2_CONTEXT, OBV2_WRAPPER_CONTEXT_URL],
151
+ id: `urn:uuid:${_learnCard.invoke.crypto().randomUUID()}`,
152
+ type: ['VerifiableCredential', 'LegacyOpenBadgeCredential'],
153
+ issuer: issuerDid,
154
+ validFrom: new Date().toISOString(),
155
+ credentialSubject: {
156
+ id: issuerDid,
157
+ },
158
+ legacyAssertion: obv2,
159
+ };
160
+
161
+ return learnCard.invoke.issueCredential(unsigned, { proofPurpose: 'assertionMethod' });
162
+ },
163
+ },
164
+ });
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Plugin } from '@learncard/core';
2
+ import { VC } from '@learncard/types';
3
+ import { VCPluginMethods } from '@learncard/vc-plugin';
4
+ import { CryptoPluginMethods } from '@learncard/crypto-plugin';
5
+
6
+ export type OpenBadgeV2PluginDependentMethods = VCPluginMethods & CryptoPluginMethods;
7
+
8
+ export type OpenBadgeV2PluginMethods = {
9
+ /**
10
+ * Wrap a legacy OpenBadge v2.0 assertion (object or URL) into a self-issued VC
11
+ */
12
+ wrapOpenBadgeV2: (obv2Assertion: object | string) => Promise<VC>;
13
+ };
14
+
15
+ export type OpenBadgeV2Plugin = Plugin<
16
+ 'OpenBadgeV2',
17
+ any,
18
+ OpenBadgeV2PluginMethods,
19
+ 'id',
20
+ OpenBadgeV2PluginDependentMethods
21
+ >;
22
+
23
+ export const OBV2_WRAPPER_CONTEXT_URL = 'https://ctx.learncard.com/wrappers/obv2/1.0.0.json';
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.