@learncard/render-method-plugin 3.0.4 → 3.0.6
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/dist/render-method-plugin.cjs.development.cjs +91 -93
- package/dist/render-method-plugin.cjs.development.cjs.map +3 -3
- package/dist/render-method-plugin.cjs.production.min.cjs +2 -2
- package/dist/render-method-plugin.cjs.production.min.cjs.map +4 -4
- package/dist/render-method-plugin.esm.js +91 -93
- package/dist/render-method-plugin.esm.js.map +3 -3
- package/package.json +65 -63
- package/src/index.ts +3 -0
- package/src/plugin.ts +150 -0
- package/src/read.ts +225 -0
- package/src/test/plugin.test.ts +652 -0
- package/src/types.ts +119 -0
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,64 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
2
|
+
"name": "@learncard/render-method-plugin",
|
|
3
|
+
"version": "3.0.6",
|
|
4
|
+
"description": "LearnCard plugin for attaching W3C renderMethod to Verifiable Credentials",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/render-method-plugin.esm.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"development": "./src/index.ts",
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/render-method-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/render-method/README.md",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/learningeconomy/LearnCard.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/learningeconomy/LearnCard/issues"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@learncard/core": "9.4.26",
|
|
43
|
+
"@learncard/helpers": "1.3.8",
|
|
44
|
+
"@learncard/types": "5.17.5",
|
|
45
|
+
"@types/jest": "^29.2.2",
|
|
46
|
+
"@types/node": "^17.0.31",
|
|
47
|
+
"aqu": "0.4.3",
|
|
48
|
+
"esbuild": "^0.14.38",
|
|
49
|
+
"esbuild-jest": "^0.5.0",
|
|
50
|
+
"esbuild-plugin-copy": "^1.3.0",
|
|
51
|
+
"jest": "^29.3.0",
|
|
52
|
+
"rimraf": "^3.0.2",
|
|
53
|
+
"shx": "^0.3.4",
|
|
54
|
+
"ts-jest": "^29.0.3"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@learncard/core": "9.4.26",
|
|
58
|
+
"@learncard/helpers": "1.3.8",
|
|
59
|
+
"@learncard/types": "5.17.5"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@learncard/helpers": "1.3.8"
|
|
63
|
+
},
|
|
64
|
+
"types": "./dist/index.d.ts",
|
|
65
|
+
"sideEffects": false
|
|
66
|
+
}
|
package/src/index.ts
ADDED
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { UnsignedVC, TemplateRenderMethod } from '@learncard/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AttachRenderMethodConfig,
|
|
5
|
+
RenderMethodDependentLearnCard,
|
|
6
|
+
RenderMethodPlugin,
|
|
7
|
+
RENDER_METHOD_CONTEXT,
|
|
8
|
+
} from './types';
|
|
9
|
+
import {
|
|
10
|
+
buildRenderData,
|
|
11
|
+
findRenderMethod,
|
|
12
|
+
findRenderMethods,
|
|
13
|
+
findTemplateRenderMethod,
|
|
14
|
+
findTemplateRenderMethods,
|
|
15
|
+
getRenderMethods,
|
|
16
|
+
getSvgMustacheRenderMethod,
|
|
17
|
+
} from './read';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The default hosted SVG Mustache template, served from `packages/render-method-templates/`.
|
|
21
|
+
*
|
|
22
|
+
* Exported so callers can opt in to the default explicitly:
|
|
23
|
+
*
|
|
24
|
+
* lc.invoke.attachRenderMethod(vc, { templateId: DEFAULT_TEMPLATE_ID })
|
|
25
|
+
*
|
|
26
|
+
* Per the opt-in contract for `attachRenderMethod`, calling without a config will NOT attach this
|
|
27
|
+
* template — callers must be explicit.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_TEMPLATE_ID = 'https://templates.learncard.com/svg/card/card-1.0.0.svg';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates a `templateId` URL. Only `http://` and `https://` schemes are accepted.
|
|
33
|
+
*
|
|
34
|
+
* Why this exists: the `templateId` is fetched at render time. Permitting `javascript:`,
|
|
35
|
+
* `file:`, or empty strings would expose the renderer to obvious script-injection or
|
|
36
|
+
* local-file-read vectors. The renderer (`renderSvgMustache`) also runs DOMPurify as a
|
|
37
|
+
* second line of defense, but blocking unsafe schemes at the source is cheaper.
|
|
38
|
+
*/
|
|
39
|
+
const isValidTemplateId = (templateId: string): boolean => {
|
|
40
|
+
if (!templateId || !templateId.trim()) return false;
|
|
41
|
+
return /^https?:\/\//i.test(templateId);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds a `TemplateRenderMethod` from the given config. Inline `templateValue` takes priority
|
|
46
|
+
* and is URL-encoded into a `data:image/svg+xml,...` URI. Otherwise `templateId` is used as-is.
|
|
47
|
+
*
|
|
48
|
+
* @throws if neither `templateId` nor `templateValue` is provided, if `templateId` is not http(s),
|
|
49
|
+
* or if `templateValue` is empty.
|
|
50
|
+
*/
|
|
51
|
+
export const buildTemplateRenderMethod = (
|
|
52
|
+
config: AttachRenderMethodConfig
|
|
53
|
+
): TemplateRenderMethod => {
|
|
54
|
+
let template: string;
|
|
55
|
+
|
|
56
|
+
if ('templateValue' in config && config.templateValue !== undefined) {
|
|
57
|
+
if (!config.templateValue.trim()) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'buildTemplateRenderMethod: `templateValue` must be a non-empty SVG string.'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
template = `data:image/svg+xml,${encodeURIComponent(config.templateValue)}`;
|
|
63
|
+
} else if ('templateId' in config && config.templateId !== undefined) {
|
|
64
|
+
if (!isValidTemplateId(config.templateId)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`buildTemplateRenderMethod: \`templateId\` must be an http(s) URL, got: ${config.templateId}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
template = config.templateId;
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'buildTemplateRenderMethod: must provide either `templateId` or `templateValue` in config.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
type: 'TemplateRenderMethod',
|
|
78
|
+
// TODO: Add other render suites (e.g., WebRenderingTemplate2022, HtmlMustacheRenderingTemplate)
|
|
79
|
+
renderSuite: 'svg-mustache',
|
|
80
|
+
template,
|
|
81
|
+
// Only include renderProperty when explicitly provided to avoid overriding VC defaults
|
|
82
|
+
...(config.renderProperty ? { renderProperty: config.renderProperty } : {}),
|
|
83
|
+
// TODO: Make mediaType configurable per renderSuite once additional suites are supported
|
|
84
|
+
outputPreference: { mediaType: 'image/svg+xml' as const },
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Attaches a `TemplateRenderMethod` to an unsigned VC, merging with any existing `renderMethod`
|
|
90
|
+
* entries. Injects the render-method JSON-LD context if not already present.
|
|
91
|
+
*
|
|
92
|
+
* **Opt-in semantics:** If `config` is omitted, the VC is returned UNCHANGED. The caller must
|
|
93
|
+
* explicitly opt in by providing `templateId` or `templateValue`. Pass `DEFAULT_TEMPLATE_ID` to
|
|
94
|
+
* use the default hosted template.
|
|
95
|
+
*
|
|
96
|
+
* This contract intentionally avoids polluting every issued credential's `@context` with the
|
|
97
|
+
* draft render-method context URL. See `RENDER_METHOD_CONTEXT` for the rationale.
|
|
98
|
+
*/
|
|
99
|
+
export const attachRenderMethod = (
|
|
100
|
+
vc: UnsignedVC,
|
|
101
|
+
config?: AttachRenderMethodConfig
|
|
102
|
+
): UnsignedVC => {
|
|
103
|
+
// Opt-in: no config → no-op. Callers must explicitly choose a template.
|
|
104
|
+
if (!config) return vc;
|
|
105
|
+
|
|
106
|
+
const renderMethod = buildTemplateRenderMethod(config);
|
|
107
|
+
|
|
108
|
+
// Normalize @context to an array so we can safely append to it.
|
|
109
|
+
const context = Array.isArray(vc['@context']) ? vc['@context'] : [vc['@context']];
|
|
110
|
+
const updatedContext = context.includes(RENDER_METHOD_CONTEXT)
|
|
111
|
+
? context
|
|
112
|
+
: [...context, RENDER_METHOD_CONTEXT];
|
|
113
|
+
|
|
114
|
+
const existingRenderMethod = vc.renderMethod;
|
|
115
|
+
let updatedRenderMethod: TemplateRenderMethod | TemplateRenderMethod[];
|
|
116
|
+
|
|
117
|
+
if (!existingRenderMethod) {
|
|
118
|
+
updatedRenderMethod = renderMethod;
|
|
119
|
+
} else if (Array.isArray(existingRenderMethod)) {
|
|
120
|
+
updatedRenderMethod = [...(existingRenderMethod as TemplateRenderMethod[]), renderMethod];
|
|
121
|
+
} else {
|
|
122
|
+
updatedRenderMethod = [existingRenderMethod as TemplateRenderMethod, renderMethod];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
...vc,
|
|
127
|
+
'@context': updatedContext,
|
|
128
|
+
renderMethod: updatedRenderMethod,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const getRenderMethodPlugin = (
|
|
133
|
+
_learnCard: RenderMethodDependentLearnCard
|
|
134
|
+
): RenderMethodPlugin => ({
|
|
135
|
+
name: 'Render Method',
|
|
136
|
+
displayName: 'Render Method Plugin',
|
|
137
|
+
description:
|
|
138
|
+
'Attaches W3C renderMethod to Verifiable Credentials for standards-based rendering',
|
|
139
|
+
methods: {
|
|
140
|
+
attachRenderMethod: (_lc, vc, config) => attachRenderMethod(vc, config),
|
|
141
|
+
buildTemplateRenderMethod: (_lc, config) => buildTemplateRenderMethod(config),
|
|
142
|
+
getRenderMethods: (_lc, vc) => getRenderMethods(vc),
|
|
143
|
+
findRenderMethod: (_lc, vc, predicate) => findRenderMethod(vc, predicate),
|
|
144
|
+
findRenderMethods: (_lc, vc, predicate) => findRenderMethods(vc, predicate),
|
|
145
|
+
findTemplateRenderMethod: (_lc, vc, suite) => findTemplateRenderMethod(vc, suite),
|
|
146
|
+
findTemplateRenderMethods: (_lc, vc, suite) => findTemplateRenderMethods(vc, suite),
|
|
147
|
+
getSvgMustacheRenderMethod: (_lc, vc) => getSvgMustacheRenderMethod(vc),
|
|
148
|
+
buildRenderData: (_lc, vc, renderProperty) => buildRenderData(vc, renderProperty),
|
|
149
|
+
},
|
|
150
|
+
});
|
package/src/read.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { unwrapBoostCredential } from '@learncard/helpers';
|
|
2
|
+
import {
|
|
3
|
+
UnsignedVC,
|
|
4
|
+
VC,
|
|
5
|
+
RenderMethod,
|
|
6
|
+
TemplateRenderMethod,
|
|
7
|
+
TemplateRenderMethodValidator,
|
|
8
|
+
} from '@learncard/types';
|
|
9
|
+
|
|
10
|
+
type AnyCredential = VC | UnsignedVC;
|
|
11
|
+
type JsonRecord = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
/** Mustache context shape produced by `buildRenderData`. */
|
|
14
|
+
export type RenderMethodData = JsonRecord;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type guard for any `TemplateRenderMethod` (Zod-validated). Use for shape
|
|
18
|
+
* checks before treating an unknown `renderMethod` entry as a template render.
|
|
19
|
+
*/
|
|
20
|
+
export const isTemplateRenderMethod = (rm: unknown): rm is TemplateRenderMethod =>
|
|
21
|
+
TemplateRenderMethodValidator.safeParse(rm).success;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Type guard for `TemplateRenderMethod` with `renderSuite === 'svg-mustache'`.
|
|
25
|
+
* Exported so consumers can compose their own selection (e.g., capability
|
|
26
|
+
* negotiation across multiple suites the renderer supports).
|
|
27
|
+
*/
|
|
28
|
+
export const isSvgMustacheRenderMethod = (rm: unknown): rm is TemplateRenderMethod => {
|
|
29
|
+
const parsed = TemplateRenderMethodValidator.safeParse(rm);
|
|
30
|
+
return parsed.success && parsed.data.renderSuite === 'svg-mustache';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns all `renderMethod` entries on a VC. Normalizes single-object form to
|
|
35
|
+
* an array and unwraps `CertifiedBoostCredential` so callers don't have to
|
|
36
|
+
* think about the wrapping layer.
|
|
37
|
+
*
|
|
38
|
+
* Entries are NOT validated here. Callers apply type guards via
|
|
39
|
+
* `findRenderMethod` / `findRenderMethods`, or one of the typed conveniences.
|
|
40
|
+
*/
|
|
41
|
+
export const getRenderMethods = (vc: AnyCredential): RenderMethod[] => {
|
|
42
|
+
const effective = (unwrapBoostCredential(vc) ?? vc) as
|
|
43
|
+
| Record<string, unknown>
|
|
44
|
+
| undefined;
|
|
45
|
+
const raw = effective?.renderMethod;
|
|
46
|
+
if (!raw) return [];
|
|
47
|
+
return (Array.isArray(raw) ? raw : [raw]) as RenderMethod[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* First `renderMethod` matching a type-guard predicate, or `null` if none.
|
|
52
|
+
*
|
|
53
|
+
* The predicate receives raw entries so it can apply Zod-validated guards
|
|
54
|
+
* (e.g., `isSvgMustacheRenderMethod`). The return type is narrowed by the
|
|
55
|
+
* predicate.
|
|
56
|
+
*/
|
|
57
|
+
export function findRenderMethod<T extends RenderMethod = RenderMethod>(
|
|
58
|
+
vc: AnyCredential,
|
|
59
|
+
predicate: (rm: RenderMethod) => rm is T
|
|
60
|
+
): T | null {
|
|
61
|
+
for (const entry of getRenderMethods(vc)) {
|
|
62
|
+
if (predicate(entry)) return entry;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Like `findRenderMethod`, but returns every matching entry. */
|
|
68
|
+
export function findRenderMethods<T extends RenderMethod = RenderMethod>(
|
|
69
|
+
vc: AnyCredential,
|
|
70
|
+
predicate: (rm: RenderMethod) => rm is T
|
|
71
|
+
): T[] {
|
|
72
|
+
return getRenderMethods(vc).filter(predicate);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* First `TemplateRenderMethod` on a VC whose `renderSuite` matches the given
|
|
77
|
+
* suite name, or `null`. Accepts a single suite string OR an array of suite
|
|
78
|
+
* strings (returns the first match across the list — useful for capability
|
|
79
|
+
* negotiation when a renderer supports multiple suites).
|
|
80
|
+
*
|
|
81
|
+
* Returns the Zod-parsed shape (not the raw entry).
|
|
82
|
+
*
|
|
83
|
+
* findTemplateRenderMethod(vc, 'svg-mustache')
|
|
84
|
+
* findTemplateRenderMethod(vc, ['svg-mustache', 'html-mustache'])
|
|
85
|
+
*/
|
|
86
|
+
export const findTemplateRenderMethod = (
|
|
87
|
+
vc: AnyCredential,
|
|
88
|
+
suite: string | readonly string[]
|
|
89
|
+
): TemplateRenderMethod | null => {
|
|
90
|
+
const suites = typeof suite === 'string' ? new Set([suite]) : new Set(suite);
|
|
91
|
+
for (const entry of getRenderMethods(vc)) {
|
|
92
|
+
const parsed = TemplateRenderMethodValidator.safeParse(entry);
|
|
93
|
+
if (parsed.success && suites.has(parsed.data.renderSuite)) {
|
|
94
|
+
return parsed.data;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Like `findTemplateRenderMethod`, but returns every matching entry. */
|
|
101
|
+
export const findTemplateRenderMethods = (
|
|
102
|
+
vc: AnyCredential,
|
|
103
|
+
suite: string | readonly string[]
|
|
104
|
+
): TemplateRenderMethod[] => {
|
|
105
|
+
const suites = typeof suite === 'string' ? new Set([suite]) : new Set(suite);
|
|
106
|
+
const result: TemplateRenderMethod[] = [];
|
|
107
|
+
for (const entry of getRenderMethods(vc)) {
|
|
108
|
+
const parsed = TemplateRenderMethodValidator.safeParse(entry);
|
|
109
|
+
if (parsed.success && suites.has(parsed.data.renderSuite)) {
|
|
110
|
+
result.push(parsed.data);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Convenience: the first svg-mustache `TemplateRenderMethod` on a VC, or `null`.
|
|
118
|
+
* Equivalent to `findTemplateRenderMethod(vc, 'svg-mustache')`.
|
|
119
|
+
*/
|
|
120
|
+
export const getSvgMustacheRenderMethod = (
|
|
121
|
+
vc: AnyCredential
|
|
122
|
+
): TemplateRenderMethod | null => findTemplateRenderMethod(vc, 'svg-mustache');
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Prototype-pollution guard. JSON Pointers come from credential data
|
|
126
|
+
* (untrusted), and these three keys are the canonical pollution vectors when
|
|
127
|
+
* a pointer is walked with bracket-indexing.
|
|
128
|
+
*/
|
|
129
|
+
const isUnsafeKey = (key: string): boolean =>
|
|
130
|
+
key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
131
|
+
|
|
132
|
+
const resolvePointer = (obj: unknown, pointer: string): unknown => {
|
|
133
|
+
if (!pointer || pointer === '/') return obj;
|
|
134
|
+
const parts = pointer
|
|
135
|
+
.replace(/^\//, '')
|
|
136
|
+
.split('/')
|
|
137
|
+
.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
138
|
+
|
|
139
|
+
if (parts.some(isUnsafeKey)) return undefined;
|
|
140
|
+
|
|
141
|
+
let current: unknown = obj;
|
|
142
|
+
for (const part of parts) {
|
|
143
|
+
if (current === null || current === undefined || typeof current !== 'object')
|
|
144
|
+
return undefined;
|
|
145
|
+
current = (current as Record<string, unknown>)[part];
|
|
146
|
+
}
|
|
147
|
+
return current;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const buildRenderDataFromPointers = (
|
|
151
|
+
vc: AnyCredential,
|
|
152
|
+
pointers: string[]
|
|
153
|
+
): JsonRecord => {
|
|
154
|
+
const result: JsonRecord = Object.create(null) as JsonRecord;
|
|
155
|
+
const isUnsafeKey = (key: string): boolean =>
|
|
156
|
+
key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
157
|
+
|
|
158
|
+
for (const pointer of pointers) {
|
|
159
|
+
const value = resolvePointer(vc, pointer);
|
|
160
|
+
if (value === undefined) continue;
|
|
161
|
+
|
|
162
|
+
const parts = pointer
|
|
163
|
+
.replace(/^\//, '')
|
|
164
|
+
.split('/')
|
|
165
|
+
.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
166
|
+
|
|
167
|
+
if (parts.some(isUnsafeKey)) continue;
|
|
168
|
+
|
|
169
|
+
let current = result;
|
|
170
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
171
|
+
const part = parts[i];
|
|
172
|
+
if (!current[part] || typeof current[part] !== 'object') {
|
|
173
|
+
current[part] = Object.create(null) as JsonRecord;
|
|
174
|
+
}
|
|
175
|
+
current = current[part] as JsonRecord;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lastPart = parts[parts.length - 1];
|
|
179
|
+
if (isUnsafeKey(lastPart)) continue;
|
|
180
|
+
current[lastPart] = value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Builds the portable Mustache context for a VC. Designed for SVG / HTML /
|
|
188
|
+
* text Mustache templates that may have been authored against any of these
|
|
189
|
+
* shapes:
|
|
190
|
+
*
|
|
191
|
+
* {{credentialSubject.name}} // direct VC paths
|
|
192
|
+
* {{credential.credentialSubject.name}} // `credential` alias
|
|
193
|
+
* {{vc.issuer.name}} // `vc` alias
|
|
194
|
+
* {{#credentialSubjects}}{{name}}{{/credentialSubjects}} // section over array
|
|
195
|
+
*
|
|
196
|
+
* The top-level credential is the single source of truth; aliases keep older
|
|
197
|
+
* templates working. When `renderProperty` (RFC 6901 JSON Pointers) is given,
|
|
198
|
+
* those values are overlaid at their pointer paths in the resulting context.
|
|
199
|
+
*
|
|
200
|
+
* This function does NOT unwrap `CertifiedBoostCredential` — pass the layer
|
|
201
|
+
* you want the template to bind to. `getRenderMethods` is the helper that
|
|
202
|
+
* unwraps.
|
|
203
|
+
*/
|
|
204
|
+
export const buildRenderData = (
|
|
205
|
+
vc: AnyCredential,
|
|
206
|
+
renderProperty?: string[]
|
|
207
|
+
): RenderMethodData => {
|
|
208
|
+
const credential = vc as unknown as JsonRecord;
|
|
209
|
+
const base: RenderMethodData = {
|
|
210
|
+
...credential,
|
|
211
|
+
vc: credential,
|
|
212
|
+
credential,
|
|
213
|
+
credentialSubjects: Array.isArray(credential.credentialSubject)
|
|
214
|
+
? credential.credentialSubject
|
|
215
|
+
: [credential.credentialSubject].filter(Boolean),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (!renderProperty || renderProperty.length === 0) return base;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
...base,
|
|
222
|
+
...buildRenderDataFromPointers(vc, renderProperty),
|
|
223
|
+
credential,
|
|
224
|
+
};
|
|
225
|
+
};
|