@learncard/open-badge-v2-plugin 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @learncard/open-badge-v2-plugin
2
+
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
+
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
+
10
+ ## Why this plugin?
11
+
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
+
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.
18
+
19
+ ## What it produces
20
+
21
+ 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
28
+
29
+ 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
+ ## Requirements
34
+
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`).
37
+
38
+ ## Installation
39
+
40
+ This package is part of the monorepo. In external usage, install from npm (when published) and ensure peer packages are available.
41
+
42
+ ```bash
43
+ # inside the monorepo
44
+ pnpm nx run open-badge-v2-plugin:build
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Below examples assume you already have a `learnCard` instance and you add both the VC plugin and this plugin.
50
+
51
+ ```ts
52
+ import { getVCPlugin } from '@learncard/vc-plugin';
53
+ import { openBadgeV2Plugin } from '@learncard/open-badge-v2-plugin';
54
+
55
+ // Ensure VC plugin is installed first (required for signing)
56
+ await learnCard.addPlugin(getVCPlugin(learnCard));
57
+
58
+ // Install the OpenBadge v2 wrapper plugin
59
+ await learnCard.addPlugin(openBadgeV2Plugin(learnCard));
60
+ ```
61
+
62
+ ### 1) Wrap from a remote HTTP(S) URL
63
+
64
+ ```ts
65
+ const url = 'https://example.org/badges/assertions/123.json';
66
+ const vc = await learnCard.invoke.wrapOpenBadgeV2(url);
67
+
68
+ // Optional: verify the signed VC
69
+ const verification = await learnCard.invoke.verifyCredential(vc);
70
+ ```
71
+
72
+ ### 2) Wrap from IPFS
73
+
74
+ ```ts
75
+ const ipfsUrl = 'ipfs://bafybeigdyr...';
76
+ const vc = await learnCard.invoke.wrapOpenBadgeV2(ipfsUrl);
77
+ ```
78
+
79
+ ### 3) Wrap from a raw OBv2 object
80
+
81
+ ```ts
82
+ 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 ...
90
+ };
91
+
92
+ const vc = await learnCard.invoke.wrapOpenBadgeV2(obv2);
93
+ ```
94
+
95
+ ### 4) Wrap from a JSON string
96
+
97
+ ```ts
98
+ const obv2String = JSON.stringify(obv2);
99
+ const vc = await learnCard.invoke.wrapOpenBadgeV2(obv2String);
100
+ ```
101
+
102
+ ## API
103
+
104
+ ```ts
105
+ wrapOpenBadgeV2(obv2Assertion: object | string): Promise<VC>
106
+ ```
107
+
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
112
+
113
+ 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
+ ## JSON‑LD Context
119
+
120
+ The wrapper vocabulary is published at:
121
+
122
+ - `https://ctx.learncard.com/wrappers/obv2/1.0.0.json`
123
+
124
+ It defines:
125
+ - `LegacyOpenBadgeCredential`
126
+ - `legacyAssertion` as `@type: @json` for embedding raw OBv2 JSON
127
+
128
+ ## Development
129
+
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`
133
+
134
+ ## Caveats
135
+
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.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,3 @@
1
+ export { openBadgeV2Plugin } from './plugin';
2
+ export * from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./open-badge-v2-plugin.cjs.production.min.js');
5
+ } else {
6
+ module.exports = require('./open-badge-v2-plugin.cjs.development.js');
7
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ OBV2_WRAPPER_CONTEXT_URL: () => OBV2_WRAPPER_CONTEXT_URL,
25
+ openBadgeV2Plugin: () => openBadgeV2Plugin
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/types.ts
30
+ var OBV2_WRAPPER_CONTEXT_URL = "https://ctx.learncard.com/wrappers/obv2/1.0.0.json";
31
+
32
+ // src/plugin.ts
33
+ var VC_V2_CONTEXT = "https://www.w3.org/ns/credentials/v2";
34
+ var isUrl = /* @__PURE__ */ __name((value) => /^(https?:)?\/\//i.test(value) || value.startsWith("ipfs://") || value.startsWith("ipns://"), "isUrl");
35
+ var isValidAndSafeUrl = /* @__PURE__ */ __name((url) => {
36
+ try {
37
+ const parsedUrl = new URL(url);
38
+ const allowedProtocols = ["https:", "http:", "ipfs:", "ipns:"];
39
+ if (!allowedProtocols.includes(parsedUrl.protocol)) {
40
+ throw new Error(
41
+ `wrapOpenBadgeV2: Protocol '${parsedUrl.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`
42
+ );
43
+ }
44
+ if (parsedUrl.protocol === "ipfs:" || parsedUrl.protocol === "ipns:") {
45
+ return;
46
+ }
47
+ const hostname = parsedUrl.hostname.toLowerCase();
48
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
49
+ throw new Error("wrapOpenBadgeV2: Access to localhost is not allowed");
50
+ }
51
+ if (hostname.startsWith("10.") || hostname.startsWith("192.168.") || hostname.startsWith("172.") && /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)) {
52
+ throw new Error("wrapOpenBadgeV2: Access to private IP ranges is not allowed");
53
+ }
54
+ if (hostname.startsWith("169.254.")) {
55
+ throw new Error("wrapOpenBadgeV2: Access to link-local addresses is not allowed");
56
+ }
57
+ } catch (error) {
58
+ if (error instanceof TypeError) {
59
+ throw new Error("wrapOpenBadgeV2: Invalid URL format");
60
+ }
61
+ throw error;
62
+ }
63
+ }, "isValidAndSafeUrl");
64
+ async function getObv2Assertion(input) {
65
+ if (typeof input === "string") {
66
+ const trimmed = input.trim();
67
+ if (!isUrl(trimmed) && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
68
+ try {
69
+ return JSON.parse(trimmed);
70
+ } catch (e) {
71
+ throw new Error("wrapOpenBadgeV2: Provided string is not a valid URL or JSON");
72
+ }
73
+ }
74
+ isValidAndSafeUrl(trimmed);
75
+ try {
76
+ const controller = new AbortController();
77
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
78
+ const res = await fetch(trimmed, {
79
+ signal: controller.signal
80
+ });
81
+ clearTimeout(timeoutId);
82
+ if (!res.ok)
83
+ throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${res.status}`);
84
+ return await res.json();
85
+ } catch (error) {
86
+ if (error instanceof TypeError) {
87
+ throw new Error(
88
+ `wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${error.message}`
89
+ );
90
+ }
91
+ if (error?.name === "AbortError") {
92
+ throw new Error("wrapOpenBadgeV2: Request timed out after 10 seconds");
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+ if (typeof input !== "object" || input === null) {
98
+ throw new Error("wrapOpenBadgeV2: Input must be a valid object");
99
+ }
100
+ return input;
101
+ }
102
+ __name(getObv2Assertion, "getObv2Assertion");
103
+ function validateObv2(obv2) {
104
+ if (!obv2 || typeof obv2 !== "object")
105
+ throw new Error("wrapOpenBadgeV2: Missing assertion");
106
+ const id = obv2.id;
107
+ if (typeof id !== "string" || !id.length)
108
+ throw new Error("wrapOpenBadgeV2: assertion.id is required");
109
+ const t = obv2.type;
110
+ const hasAssertionType = typeof t === "string" && t === "Assertion" || Array.isArray(t) && t.includes("Assertion");
111
+ if (!hasAssertionType)
112
+ throw new Error("wrapOpenBadgeV2: assertion.type must include 'Assertion'");
113
+ if (!obv2.issuedOn || typeof obv2.issuedOn !== "string") {
114
+ throw new Error("wrapOpenBadgeV2: assertion.issuedOn (string) is required");
115
+ }
116
+ }
117
+ __name(validateObv2, "validateObv2");
118
+ var openBadgeV2Plugin = /* @__PURE__ */ __name((learnCard) => ({
119
+ name: "OpenBadgeV2",
120
+ displayName: "OpenBadge v2 Wrapper",
121
+ description: "Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials",
122
+ methods: {
123
+ wrapOpenBadgeV2: async (_learnCard, obv2Assertion) => {
124
+ const issuerDid = learnCard.id.did();
125
+ const obv2 = await getObv2Assertion(obv2Assertion);
126
+ validateObv2(obv2);
127
+ const unsigned = {
128
+ "@context": [VC_V2_CONTEXT, OBV2_WRAPPER_CONTEXT_URL],
129
+ id: `urn:uuid:${_learnCard.invoke.crypto().randomUUID()}`,
130
+ type: ["VerifiableCredential", "LegacyOpenBadgeCredential"],
131
+ issuer: issuerDid,
132
+ validFrom: new Date().toISOString(),
133
+ credentialSubject: {
134
+ id: issuerDid
135
+ },
136
+ legacyAssertion: obv2
137
+ };
138
+ return learnCard.invoke.issueCredential(unsigned, { proofPurpose: "assertionMethod" });
139
+ }
140
+ }
141
+ }), "openBadgeV2Plugin");
142
+ //# sourceMappingURL=open-badge-v2-plugin.cjs.development.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/types.ts", "../src/plugin.ts"],
4
+ "sourcesContent": ["export { openBadgeV2Plugin } from './plugin';\nexport * from './types';\n", "import { Plugin } from '@learncard/core';\nimport { VC } from '@learncard/types';\nimport { VCPluginMethods } from '@learncard/vc-plugin';\nimport { CryptoPluginMethods } from '@learncard/crypto-plugin';\n\nexport type OpenBadgeV2PluginDependentMethods = VCPluginMethods & CryptoPluginMethods;\n\nexport type OpenBadgeV2PluginMethods = {\n /**\n * Wrap a legacy OpenBadge v2.0 assertion (object or URL) into a self-issued VC\n */\n wrapOpenBadgeV2: (obv2Assertion: object | string) => Promise<VC>;\n};\n\nexport type OpenBadgeV2Plugin = Plugin<\n 'OpenBadgeV2',\n any,\n OpenBadgeV2PluginMethods,\n 'id',\n OpenBadgeV2PluginDependentMethods\n>;\n\nexport const OBV2_WRAPPER_CONTEXT_URL = 'https://ctx.learncard.com/wrappers/obv2/1.0.0.json';\n", "import { LearnCard } from '@learncard/core';\nimport { UnsignedVC, VC } from '@learncard/types';\n\nimport {\n OpenBadgeV2Plugin,\n OBV2_WRAPPER_CONTEXT_URL,\n OpenBadgeV2PluginDependentMethods,\n} from './types';\n\nconst VC_V2_CONTEXT = 'https://www.w3.org/ns/credentials/v2';\n\nconst isUrl = (value: string): boolean =>\n /^(https?:)?\\/\\//i.test(value) || value.startsWith('ipfs://') || value.startsWith('ipns://');\n\nconst isValidAndSafeUrl = (url: string): void => {\n try {\n const parsedUrl = new URL(url);\n\n // Only allow http, https, ipfs, ipns protocols\n const allowedProtocols = ['https:', 'http:', 'ipfs:', 'ipns:'];\n if (!allowedProtocols.includes(parsedUrl.protocol)) {\n throw new Error(\n `wrapOpenBadgeV2: Protocol '${parsedUrl.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`\n );\n }\n\n // Skip internal network checks for IPFS/IPNS as they don't use traditional IP addresses\n if (parsedUrl.protocol === 'ipfs:' || parsedUrl.protocol === 'ipns:') {\n return;\n }\n\n // Block localhost and internal networks for HTTP(S)\n const hostname = parsedUrl.hostname.toLowerCase();\n\n // Block localhost variations\n if (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '0.0.0.0' ||\n hostname === '::1'\n ) {\n throw new Error('wrapOpenBadgeV2: Access to localhost is not allowed');\n }\n\n // Block private IP ranges (RFC 1918)\n if (\n hostname.startsWith('10.') ||\n hostname.startsWith('192.168.') ||\n (hostname.startsWith('172.') && /^172\\.(1[6-9]|2[0-9]|3[0-1])\\./.test(hostname))\n ) {\n throw new Error('wrapOpenBadgeV2: Access to private IP ranges is not allowed');\n }\n\n // Block link-local addresses\n if (hostname.startsWith('169.254.')) {\n throw new Error('wrapOpenBadgeV2: Access to link-local addresses is not allowed');\n }\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error('wrapOpenBadgeV2: Invalid URL format');\n }\n throw error;\n }\n};\n\nasync function getObv2Assertion(input: object | string): Promise<Record<string, any>> {\n if (typeof input === 'string') {\n // Treat string as URL. If it looks like raw JSON, try to parse for developer ergonomics\n const trimmed = input.trim();\n if (!isUrl(trimmed) && (trimmed.startsWith('{') || trimmed.startsWith('['))) {\n try {\n return JSON.parse(trimmed);\n } catch (e) {\n throw new Error('wrapOpenBadgeV2: Provided string is not a valid URL or JSON');\n }\n }\n\n // Validate URL for security\n isValidAndSafeUrl(trimmed);\n\n try {\n // Add timeout to prevent hanging requests (10 seconds)\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 10000);\n\n const res = await fetch(trimmed, {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!res.ok)\n throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${res.status}`);\n\n return await res.json();\n } catch (error: any) {\n if (error instanceof TypeError) {\n throw new Error(\n `wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${error.message}`\n );\n }\n if (error?.name === 'AbortError') {\n throw new Error('wrapOpenBadgeV2: Request timed out after 10 seconds');\n }\n throw error;\n }\n }\n\n // Add basic type validation before assertion\n if (typeof input !== 'object' || input === null) {\n throw new Error('wrapOpenBadgeV2: Input must be a valid object');\n }\n\n return input as Record<string, any>;\n}\n\nfunction validateObv2(obv2: Record<string, any>): void {\n if (!obv2 || typeof obv2 !== 'object') throw new Error('wrapOpenBadgeV2: Missing assertion');\n\n const id = obv2.id;\n if (typeof id !== 'string' || !id.length)\n throw new Error('wrapOpenBadgeV2: assertion.id is required');\n\n const t = obv2.type;\n const hasAssertionType =\n (typeof t === 'string' && t === 'Assertion') ||\n (Array.isArray(t) && t.includes('Assertion'));\n if (!hasAssertionType)\n throw new Error(\"wrapOpenBadgeV2: assertion.type must include 'Assertion'\");\n\n if (!obv2.issuedOn || typeof obv2.issuedOn !== 'string') {\n throw new Error('wrapOpenBadgeV2: assertion.issuedOn (string) is required');\n }\n}\n\nexport const openBadgeV2Plugin = (\n learnCard: LearnCard<any, 'id', OpenBadgeV2PluginDependentMethods>\n): OpenBadgeV2Plugin => ({\n name: 'OpenBadgeV2',\n displayName: 'OpenBadge v2 Wrapper',\n description: 'Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials',\n methods: {\n wrapOpenBadgeV2: async (_learnCard, obv2Assertion: object | string): Promise<VC> => {\n const issuerDid = learnCard.id.did();\n\n const obv2 = await getObv2Assertion(obv2Assertion);\n validateObv2(obv2);\n\n const unsigned: UnsignedVC = {\n '@context': [VC_V2_CONTEXT, OBV2_WRAPPER_CONTEXT_URL],\n id: `urn:uuid:${_learnCard.invoke.crypto().randomUUID()}`,\n type: ['VerifiableCredential', 'LegacyOpenBadgeCredential'],\n issuer: issuerDid,\n validFrom: new Date().toISOString(),\n credentialSubject: {\n id: issuerDid,\n },\n legacyAssertion: obv2,\n };\n\n return learnCard.invoke.issueCredential(unsigned, { proofPurpose: 'assertionMethod' });\n },\n },\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";var i=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var c=Object.prototype.hasOwnProperty;var s=(r,e)=>i(r,"name",{value:e,configurable:!0});var w=(r,e)=>{for(var t in e)i(r,t,{get:e[t],enumerable:!0})},h=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of l(e))!c.call(r,n)&&n!==t&&i(r,n,{get:()=>e[n],enumerable:!(o=g(e,n))||o.enumerable});return r};var f=r=>h(i({},"__esModule",{value:!0}),r);var B={};w(B,{OBV2_WRAPPER_CONTEXT_URL:()=>a,openBadgeV2Plugin:()=>d});module.exports=f(B);var a="https://ctx.learncard.com/wrappers/obv2/1.0.0.json";var u="https://www.w3.org/ns/credentials/v2",O=s(r=>/^(https?:)?\/\//i.test(r)||r.startsWith("ipfs://")||r.startsWith("ipns://"),"isUrl"),V=s(r=>{try{let e=new URL(r);if(!["https:","http:","ipfs:","ipns:"].includes(e.protocol))throw new Error(`wrapOpenBadgeV2: Protocol '${e.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`);if(e.protocol==="ipfs:"||e.protocol==="ipns:")return;let o=e.hostname.toLowerCase();if(o==="localhost"||o==="127.0.0.1"||o==="0.0.0.0"||o==="::1")throw new Error("wrapOpenBadgeV2: Access to localhost is not allowed");if(o.startsWith("10.")||o.startsWith("192.168.")||o.startsWith("172.")&&/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(o))throw new Error("wrapOpenBadgeV2: Access to private IP ranges is not allowed");if(o.startsWith("169.254."))throw new Error("wrapOpenBadgeV2: Access to link-local addresses is not allowed")}catch(e){throw e instanceof TypeError?new Error("wrapOpenBadgeV2: Invalid URL format"):e}},"isValidAndSafeUrl");async function m(r){if(typeof r=="string"){let e=r.trim();if(!O(e)&&(e.startsWith("{")||e.startsWith("[")))try{return JSON.parse(e)}catch{throw new Error("wrapOpenBadgeV2: Provided string is not a valid URL or JSON")}V(e);try{let t=new AbortController,o=setTimeout(()=>t.abort(),1e4),n=await fetch(e,{signal:t.signal});if(clearTimeout(o),!n.ok)throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${n.status}`);return await n.json()}catch(t){throw t instanceof TypeError?new Error(`wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${t.message}`):t?.name==="AbortError"?new Error("wrapOpenBadgeV2: Request timed out after 10 seconds"):t}}if(typeof r!="object"||r===null)throw new Error("wrapOpenBadgeV2: Input must be a valid object");return r}s(m,"getObv2Assertion");function y(r){if(!r||typeof r!="object")throw new Error("wrapOpenBadgeV2: Missing assertion");let e=r.id;if(typeof e!="string"||!e.length)throw new Error("wrapOpenBadgeV2: assertion.id is required");let t=r.type;if(!(typeof t=="string"&&t==="Assertion"||Array.isArray(t)&&t.includes("Assertion")))throw new Error("wrapOpenBadgeV2: assertion.type must include 'Assertion'");if(!r.issuedOn||typeof r.issuedOn!="string")throw new Error("wrapOpenBadgeV2: assertion.issuedOn (string) is required")}s(y,"validateObv2");var d=s(r=>({name:"OpenBadgeV2",displayName:"OpenBadge v2 Wrapper",description:"Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials",methods:{wrapOpenBadgeV2:async(e,t)=>{let o=r.id.did(),n=await m(t);y(n);let p={"@context":[u,a],id:`urn:uuid:${e.invoke.crypto().randomUUID()}`,type:["VerifiableCredential","LegacyOpenBadgeCredential"],issuer:o,validFrom:new Date().toISOString(),credentialSubject:{id:o},legacyAssertion:n};return r.invoke.issueCredential(p,{proofPurpose:"assertionMethod"})}}}),"openBadgeV2Plugin");
2
+ //# sourceMappingURL=open-badge-v2-plugin.cjs.production.min.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/types.ts", "../src/plugin.ts"],
4
+ "sourcesContent": ["export { openBadgeV2Plugin } from './plugin';\nexport * from './types';\n", "import { Plugin } from '@learncard/core';\nimport { VC } from '@learncard/types';\nimport { VCPluginMethods } from '@learncard/vc-plugin';\nimport { CryptoPluginMethods } from '@learncard/crypto-plugin';\n\nexport type OpenBadgeV2PluginDependentMethods = VCPluginMethods & CryptoPluginMethods;\n\nexport type OpenBadgeV2PluginMethods = {\n /**\n * Wrap a legacy OpenBadge v2.0 assertion (object or URL) into a self-issued VC\n */\n wrapOpenBadgeV2: (obv2Assertion: object | string) => Promise<VC>;\n};\n\nexport type OpenBadgeV2Plugin = Plugin<\n 'OpenBadgeV2',\n any,\n OpenBadgeV2PluginMethods,\n 'id',\n OpenBadgeV2PluginDependentMethods\n>;\n\nexport const OBV2_WRAPPER_CONTEXT_URL = 'https://ctx.learncard.com/wrappers/obv2/1.0.0.json';\n", "import { LearnCard } from '@learncard/core';\nimport { UnsignedVC, VC } from '@learncard/types';\n\nimport {\n OpenBadgeV2Plugin,\n OBV2_WRAPPER_CONTEXT_URL,\n OpenBadgeV2PluginDependentMethods,\n} from './types';\n\nconst VC_V2_CONTEXT = 'https://www.w3.org/ns/credentials/v2';\n\nconst isUrl = (value: string): boolean =>\n /^(https?:)?\\/\\//i.test(value) || value.startsWith('ipfs://') || value.startsWith('ipns://');\n\nconst isValidAndSafeUrl = (url: string): void => {\n try {\n const parsedUrl = new URL(url);\n\n // Only allow http, https, ipfs, ipns protocols\n const allowedProtocols = ['https:', 'http:', 'ipfs:', 'ipns:'];\n if (!allowedProtocols.includes(parsedUrl.protocol)) {\n throw new Error(\n `wrapOpenBadgeV2: Protocol '${parsedUrl.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`\n );\n }\n\n // Skip internal network checks for IPFS/IPNS as they don't use traditional IP addresses\n if (parsedUrl.protocol === 'ipfs:' || parsedUrl.protocol === 'ipns:') {\n return;\n }\n\n // Block localhost and internal networks for HTTP(S)\n const hostname = parsedUrl.hostname.toLowerCase();\n\n // Block localhost variations\n if (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '0.0.0.0' ||\n hostname === '::1'\n ) {\n throw new Error('wrapOpenBadgeV2: Access to localhost is not allowed');\n }\n\n // Block private IP ranges (RFC 1918)\n if (\n hostname.startsWith('10.') ||\n hostname.startsWith('192.168.') ||\n (hostname.startsWith('172.') && /^172\\.(1[6-9]|2[0-9]|3[0-1])\\./.test(hostname))\n ) {\n throw new Error('wrapOpenBadgeV2: Access to private IP ranges is not allowed');\n }\n\n // Block link-local addresses\n if (hostname.startsWith('169.254.')) {\n throw new Error('wrapOpenBadgeV2: Access to link-local addresses is not allowed');\n }\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error('wrapOpenBadgeV2: Invalid URL format');\n }\n throw error;\n }\n};\n\nasync function getObv2Assertion(input: object | string): Promise<Record<string, any>> {\n if (typeof input === 'string') {\n // Treat string as URL. If it looks like raw JSON, try to parse for developer ergonomics\n const trimmed = input.trim();\n if (!isUrl(trimmed) && (trimmed.startsWith('{') || trimmed.startsWith('['))) {\n try {\n return JSON.parse(trimmed);\n } catch (e) {\n throw new Error('wrapOpenBadgeV2: Provided string is not a valid URL or JSON');\n }\n }\n\n // Validate URL for security\n isValidAndSafeUrl(trimmed);\n\n try {\n // Add timeout to prevent hanging requests (10 seconds)\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 10000);\n\n const res = await fetch(trimmed, {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!res.ok)\n throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${res.status}`);\n\n return await res.json();\n } catch (error: any) {\n if (error instanceof TypeError) {\n throw new Error(\n `wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${error.message}`\n );\n }\n if (error?.name === 'AbortError') {\n throw new Error('wrapOpenBadgeV2: Request timed out after 10 seconds');\n }\n throw error;\n }\n }\n\n // Add basic type validation before assertion\n if (typeof input !== 'object' || input === null) {\n throw new Error('wrapOpenBadgeV2: Input must be a valid object');\n }\n\n return input as Record<string, any>;\n}\n\nfunction validateObv2(obv2: Record<string, any>): void {\n if (!obv2 || typeof obv2 !== 'object') throw new Error('wrapOpenBadgeV2: Missing assertion');\n\n const id = obv2.id;\n if (typeof id !== 'string' || !id.length)\n throw new Error('wrapOpenBadgeV2: assertion.id is required');\n\n const t = obv2.type;\n const hasAssertionType =\n (typeof t === 'string' && t === 'Assertion') ||\n (Array.isArray(t) && t.includes('Assertion'));\n if (!hasAssertionType)\n throw new Error(\"wrapOpenBadgeV2: assertion.type must include 'Assertion'\");\n\n if (!obv2.issuedOn || typeof obv2.issuedOn !== 'string') {\n throw new Error('wrapOpenBadgeV2: assertion.issuedOn (string) is required');\n }\n}\n\nexport const openBadgeV2Plugin = (\n learnCard: LearnCard<any, 'id', OpenBadgeV2PluginDependentMethods>\n): OpenBadgeV2Plugin => ({\n name: 'OpenBadgeV2',\n displayName: 'OpenBadge v2 Wrapper',\n description: 'Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials',\n methods: {\n wrapOpenBadgeV2: async (_learnCard, obv2Assertion: object | string): Promise<VC> => {\n const issuerDid = learnCard.id.did();\n\n const obv2 = await getObv2Assertion(obv2Assertion);\n validateObv2(obv2);\n\n const unsigned: UnsignedVC = {\n '@context': [VC_V2_CONTEXT, OBV2_WRAPPER_CONTEXT_URL],\n id: `urn:uuid:${_learnCard.invoke.crypto().randomUUID()}`,\n type: ['VerifiableCredential', 'LegacyOpenBadgeCredential'],\n issuer: issuerDid,\n validFrom: new Date().toISOString(),\n credentialSubject: {\n id: issuerDid,\n },\n legacyAssertion: obv2,\n };\n\n return learnCard.invoke.issueCredential(unsigned, { proofPurpose: 'assertionMethod' });\n },\n },\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["src_exports", "__export", "OBV2_WRAPPER_CONTEXT_URL", "openBadgeV2Plugin", "__toCommonJS", "OBV2_WRAPPER_CONTEXT_URL", "VC_V2_CONTEXT", "isUrl", "__name", "value", "isValidAndSafeUrl", "url", "parsedUrl", "hostname", "error", "getObv2Assertion", "input", "trimmed", "controller", "timeoutId", "res", "validateObv2", "obv2", "id", "openBadgeV2Plugin", "learnCard", "_learnCard", "obv2Assertion", "issuerDid", "unsigned", "OBV2_WRAPPER_CONTEXT_URL"]
7
+ }
@@ -0,0 +1,121 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/types.ts
5
+ var OBV2_WRAPPER_CONTEXT_URL = "https://ctx.learncard.com/wrappers/obv2/1.0.0.json";
6
+
7
+ // src/plugin.ts
8
+ var VC_V2_CONTEXT = "https://www.w3.org/ns/credentials/v2";
9
+ var isUrl = /* @__PURE__ */ __name((value) => /^(https?:)?\/\//i.test(value) || value.startsWith("ipfs://") || value.startsWith("ipns://"), "isUrl");
10
+ var isValidAndSafeUrl = /* @__PURE__ */ __name((url) => {
11
+ try {
12
+ const parsedUrl = new URL(url);
13
+ const allowedProtocols = ["https:", "http:", "ipfs:", "ipns:"];
14
+ if (!allowedProtocols.includes(parsedUrl.protocol)) {
15
+ throw new Error(
16
+ `wrapOpenBadgeV2: Protocol '${parsedUrl.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`
17
+ );
18
+ }
19
+ if (parsedUrl.protocol === "ipfs:" || parsedUrl.protocol === "ipns:") {
20
+ return;
21
+ }
22
+ const hostname = parsedUrl.hostname.toLowerCase();
23
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
24
+ throw new Error("wrapOpenBadgeV2: Access to localhost is not allowed");
25
+ }
26
+ if (hostname.startsWith("10.") || hostname.startsWith("192.168.") || hostname.startsWith("172.") && /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)) {
27
+ throw new Error("wrapOpenBadgeV2: Access to private IP ranges is not allowed");
28
+ }
29
+ if (hostname.startsWith("169.254.")) {
30
+ throw new Error("wrapOpenBadgeV2: Access to link-local addresses is not allowed");
31
+ }
32
+ } catch (error) {
33
+ if (error instanceof TypeError) {
34
+ throw new Error("wrapOpenBadgeV2: Invalid URL format");
35
+ }
36
+ throw error;
37
+ }
38
+ }, "isValidAndSafeUrl");
39
+ async function getObv2Assertion(input) {
40
+ if (typeof input === "string") {
41
+ const trimmed = input.trim();
42
+ if (!isUrl(trimmed) && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
43
+ try {
44
+ return JSON.parse(trimmed);
45
+ } catch (e) {
46
+ throw new Error("wrapOpenBadgeV2: Provided string is not a valid URL or JSON");
47
+ }
48
+ }
49
+ isValidAndSafeUrl(trimmed);
50
+ try {
51
+ const controller = new AbortController();
52
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
53
+ const res = await fetch(trimmed, {
54
+ signal: controller.signal
55
+ });
56
+ clearTimeout(timeoutId);
57
+ if (!res.ok)
58
+ throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${res.status}`);
59
+ return await res.json();
60
+ } catch (error) {
61
+ if (error instanceof TypeError) {
62
+ throw new Error(
63
+ `wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${error.message}`
64
+ );
65
+ }
66
+ if (error?.name === "AbortError") {
67
+ throw new Error("wrapOpenBadgeV2: Request timed out after 10 seconds");
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ if (typeof input !== "object" || input === null) {
73
+ throw new Error("wrapOpenBadgeV2: Input must be a valid object");
74
+ }
75
+ return input;
76
+ }
77
+ __name(getObv2Assertion, "getObv2Assertion");
78
+ function validateObv2(obv2) {
79
+ if (!obv2 || typeof obv2 !== "object")
80
+ throw new Error("wrapOpenBadgeV2: Missing assertion");
81
+ const id = obv2.id;
82
+ if (typeof id !== "string" || !id.length)
83
+ throw new Error("wrapOpenBadgeV2: assertion.id is required");
84
+ const t = obv2.type;
85
+ const hasAssertionType = typeof t === "string" && t === "Assertion" || Array.isArray(t) && t.includes("Assertion");
86
+ if (!hasAssertionType)
87
+ throw new Error("wrapOpenBadgeV2: assertion.type must include 'Assertion'");
88
+ if (!obv2.issuedOn || typeof obv2.issuedOn !== "string") {
89
+ throw new Error("wrapOpenBadgeV2: assertion.issuedOn (string) is required");
90
+ }
91
+ }
92
+ __name(validateObv2, "validateObv2");
93
+ var openBadgeV2Plugin = /* @__PURE__ */ __name((learnCard) => ({
94
+ name: "OpenBadgeV2",
95
+ displayName: "OpenBadge v2 Wrapper",
96
+ description: "Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials",
97
+ methods: {
98
+ wrapOpenBadgeV2: async (_learnCard, obv2Assertion) => {
99
+ const issuerDid = learnCard.id.did();
100
+ const obv2 = await getObv2Assertion(obv2Assertion);
101
+ validateObv2(obv2);
102
+ const unsigned = {
103
+ "@context": [VC_V2_CONTEXT, OBV2_WRAPPER_CONTEXT_URL],
104
+ id: `urn:uuid:${_learnCard.invoke.crypto().randomUUID()}`,
105
+ type: ["VerifiableCredential", "LegacyOpenBadgeCredential"],
106
+ issuer: issuerDid,
107
+ validFrom: new Date().toISOString(),
108
+ credentialSubject: {
109
+ id: issuerDid
110
+ },
111
+ legacyAssertion: obv2
112
+ };
113
+ return learnCard.invoke.issueCredential(unsigned, { proofPurpose: "assertionMethod" });
114
+ }
115
+ }
116
+ }), "openBadgeV2Plugin");
117
+ export {
118
+ OBV2_WRAPPER_CONTEXT_URL,
119
+ openBadgeV2Plugin
120
+ };
121
+ //# sourceMappingURL=open-badge-v2-plugin.esm.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/types.ts", "../src/plugin.ts"],
4
+ "sourcesContent": ["import { Plugin } from '@learncard/core';\nimport { VC } from '@learncard/types';\nimport { VCPluginMethods } from '@learncard/vc-plugin';\nimport { CryptoPluginMethods } from '@learncard/crypto-plugin';\n\nexport type OpenBadgeV2PluginDependentMethods = VCPluginMethods & CryptoPluginMethods;\n\nexport type OpenBadgeV2PluginMethods = {\n /**\n * Wrap a legacy OpenBadge v2.0 assertion (object or URL) into a self-issued VC\n */\n wrapOpenBadgeV2: (obv2Assertion: object | string) => Promise<VC>;\n};\n\nexport type OpenBadgeV2Plugin = Plugin<\n 'OpenBadgeV2',\n any,\n OpenBadgeV2PluginMethods,\n 'id',\n OpenBadgeV2PluginDependentMethods\n>;\n\nexport const OBV2_WRAPPER_CONTEXT_URL = 'https://ctx.learncard.com/wrappers/obv2/1.0.0.json';\n", "import { LearnCard } from '@learncard/core';\nimport { UnsignedVC, VC } from '@learncard/types';\n\nimport {\n OpenBadgeV2Plugin,\n OBV2_WRAPPER_CONTEXT_URL,\n OpenBadgeV2PluginDependentMethods,\n} from './types';\n\nconst VC_V2_CONTEXT = 'https://www.w3.org/ns/credentials/v2';\n\nconst isUrl = (value: string): boolean =>\n /^(https?:)?\\/\\//i.test(value) || value.startsWith('ipfs://') || value.startsWith('ipns://');\n\nconst isValidAndSafeUrl = (url: string): void => {\n try {\n const parsedUrl = new URL(url);\n\n // Only allow http, https, ipfs, ipns protocols\n const allowedProtocols = ['https:', 'http:', 'ipfs:', 'ipns:'];\n if (!allowedProtocols.includes(parsedUrl.protocol)) {\n throw new Error(\n `wrapOpenBadgeV2: Protocol '${parsedUrl.protocol}' is not allowed. Only HTTP(S), IPFS, and IPNS are supported`\n );\n }\n\n // Skip internal network checks for IPFS/IPNS as they don't use traditional IP addresses\n if (parsedUrl.protocol === 'ipfs:' || parsedUrl.protocol === 'ipns:') {\n return;\n }\n\n // Block localhost and internal networks for HTTP(S)\n const hostname = parsedUrl.hostname.toLowerCase();\n\n // Block localhost variations\n if (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '0.0.0.0' ||\n hostname === '::1'\n ) {\n throw new Error('wrapOpenBadgeV2: Access to localhost is not allowed');\n }\n\n // Block private IP ranges (RFC 1918)\n if (\n hostname.startsWith('10.') ||\n hostname.startsWith('192.168.') ||\n (hostname.startsWith('172.') && /^172\\.(1[6-9]|2[0-9]|3[0-1])\\./.test(hostname))\n ) {\n throw new Error('wrapOpenBadgeV2: Access to private IP ranges is not allowed');\n }\n\n // Block link-local addresses\n if (hostname.startsWith('169.254.')) {\n throw new Error('wrapOpenBadgeV2: Access to link-local addresses is not allowed');\n }\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error('wrapOpenBadgeV2: Invalid URL format');\n }\n throw error;\n }\n};\n\nasync function getObv2Assertion(input: object | string): Promise<Record<string, any>> {\n if (typeof input === 'string') {\n // Treat string as URL. If it looks like raw JSON, try to parse for developer ergonomics\n const trimmed = input.trim();\n if (!isUrl(trimmed) && (trimmed.startsWith('{') || trimmed.startsWith('['))) {\n try {\n return JSON.parse(trimmed);\n } catch (e) {\n throw new Error('wrapOpenBadgeV2: Provided string is not a valid URL or JSON');\n }\n }\n\n // Validate URL for security\n isValidAndSafeUrl(trimmed);\n\n try {\n // Add timeout to prevent hanging requests (10 seconds)\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 10000);\n\n const res = await fetch(trimmed, {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!res.ok)\n throw new Error(`wrapOpenBadgeV2: Failed to fetch OBv2 assertion: ${res.status}`);\n\n return await res.json();\n } catch (error: any) {\n if (error instanceof TypeError) {\n throw new Error(\n `wrapOpenBadgeV2: Network error while fetching OBv2 assertion: ${error.message}`\n );\n }\n if (error?.name === 'AbortError') {\n throw new Error('wrapOpenBadgeV2: Request timed out after 10 seconds');\n }\n throw error;\n }\n }\n\n // Add basic type validation before assertion\n if (typeof input !== 'object' || input === null) {\n throw new Error('wrapOpenBadgeV2: Input must be a valid object');\n }\n\n return input as Record<string, any>;\n}\n\nfunction validateObv2(obv2: Record<string, any>): void {\n if (!obv2 || typeof obv2 !== 'object') throw new Error('wrapOpenBadgeV2: Missing assertion');\n\n const id = obv2.id;\n if (typeof id !== 'string' || !id.length)\n throw new Error('wrapOpenBadgeV2: assertion.id is required');\n\n const t = obv2.type;\n const hasAssertionType =\n (typeof t === 'string' && t === 'Assertion') ||\n (Array.isArray(t) && t.includes('Assertion'));\n if (!hasAssertionType)\n throw new Error(\"wrapOpenBadgeV2: assertion.type must include 'Assertion'\");\n\n if (!obv2.issuedOn || typeof obv2.issuedOn !== 'string') {\n throw new Error('wrapOpenBadgeV2: assertion.issuedOn (string) is required');\n }\n}\n\nexport const openBadgeV2Plugin = (\n learnCard: LearnCard<any, 'id', OpenBadgeV2PluginDependentMethods>\n): OpenBadgeV2Plugin => ({\n name: 'OpenBadgeV2',\n displayName: 'OpenBadge v2 Wrapper',\n description: 'Wrap legacy OpenBadge v2.0 assertions into self-issued Verifiable Credentials',\n methods: {\n wrapOpenBadgeV2: async (_learnCard, obv2Assertion: object | string): Promise<VC> => {\n const issuerDid = learnCard.id.did();\n\n const obv2 = await getObv2Assertion(obv2Assertion);\n validateObv2(obv2);\n\n const unsigned: UnsignedVC = {\n '@context': [VC_V2_CONTEXT, OBV2_WRAPPER_CONTEXT_URL],\n id: `urn:uuid:${_learnCard.invoke.crypto().randomUUID()}`,\n type: ['VerifiableCredential', 'LegacyOpenBadgeCredential'],\n issuer: issuerDid,\n validFrom: new Date().toISOString(),\n credentialSubject: {\n id: issuerDid,\n },\n legacyAssertion: obv2,\n };\n\n return learnCard.invoke.issueCredential(unsigned, { proofPurpose: 'assertionMethod' });\n },\n },\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,4 @@
1
+ import { LearnCard } from '@learncard/core';
2
+ import { OpenBadgeV2Plugin, OpenBadgeV2PluginDependentMethods } from './types';
3
+ export declare const openBadgeV2Plugin: (learnCard: LearnCard<any, "id", OpenBadgeV2PluginDependentMethods>) => OpenBadgeV2Plugin;
4
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EACH,iBAAiB,EAEjB,iCAAiC,EACpC,MAAM,SAAS,CAAC;AAgIjB,eAAO,MAAM,iBAAiB,cACf,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,iCAAiC,CAAC,KACnE,iBA0BD,CAAC"}
@@ -0,0 +1,14 @@
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
+ export type OpenBadgeV2PluginDependentMethods = VCPluginMethods & CryptoPluginMethods;
6
+ export type OpenBadgeV2PluginMethods = {
7
+ /**
8
+ * Wrap a legacy OpenBadge v2.0 assertion (object or URL) into a self-issued VC
9
+ */
10
+ wrapOpenBadgeV2: (obv2Assertion: object | string) => Promise<VC>;
11
+ };
12
+ export type OpenBadgeV2Plugin = Plugin<'OpenBadgeV2', any, OpenBadgeV2PluginMethods, 'id', OpenBadgeV2PluginDependentMethods>;
13
+ export declare const OBV2_WRAPPER_CONTEXT_URL = "https://ctx.learncard.com/wrappers/obv2/1.0.0.json";
14
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,MAAM,MAAM,iCAAiC,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAEtF,MAAM,MAAM,wBAAwB,GAAG;IACnC;;OAEG;IACH,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;CACpE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAClC,aAAa,EACb,GAAG,EACH,wBAAwB,EACxB,IAAI,EACJ,iCAAiC,CACpC,CAAC;AAEF,eAAO,MAAM,wBAAwB,uDAAuD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@learncard/open-badge-v2-plugin",
3
+ "version": "1.0.0",
4
+ "description": "OpenBadge v2 wrapper plugin for LearnCard. Wraps OBv2 assertions into self-issued VCs.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/open-badge-v2-plugin.esm.js",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "author": "Learning Economy Foundation (www.learningeconomy.io)",
11
+ "license": "MIT",
12
+ "homepage": "https://github.com/WeLibraryOS/LearnCard#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/WeLibraryOS/LearnCard.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/WeLibraryOS/LearnCard/issues"
19
+ },
20
+ "devDependencies": {
21
+ "@types/jest": "^29.2.2",
22
+ "@types/node": "^17.0.31",
23
+ "aqu": "0.4.3",
24
+ "esbuild": "^0.14.38",
25
+ "esbuild-jest": "^0.5.0",
26
+ "esbuild-plugin-copy": "^1.3.0",
27
+ "jest": "^29.3.0",
28
+ "shx": "^0.3.4",
29
+ "ts-jest": "^29.0.3"
30
+ },
31
+ "types": "./dist/index.d.ts",
32
+ "dependencies": {
33
+ "@learncard/core": "9.3.35",
34
+ "@learncard/vc-plugin": "1.1.57",
35
+ "@learncard/crypto-plugin": "1.0.46",
36
+ "@learncard/types": "5.8.3"
37
+ },
38
+ "scripts": {
39
+ "build": "node ./scripts/build.mjs && shx cp ./scripts/mixedEntypoint.js ./dist/index.js && tsc --p tsconfig.json",
40
+ "test": "jest --passWithNoTests",
41
+ "test:watch": "jest --watch",
42
+ "test:coverage": "jest --silent --ci --coverage --coverageReporters=\"text\" --coverageReporters=\"text-summary\""
43
+ }
44
+ }