From 08c2c5b74fbb99757978814217e33d2f94365310 Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Tue, 11 Nov 2025 17:42:10 -0800 Subject: [PATCH 1/8] Add NFC renderer for verifiable credentials. Implements NFC rendering support for verifiable credentials following the W3C VC Rendering Methods specification. Provides both static and dynamic rendering modes: - Static mode: Decodes pre-encoded payloads from template/payload fields - Dynamic mode: Extracts credential data using JSON pointer paths - Supports multibase (base58/base64url) and data URI encoding - Compatible with W3C spec renderSuite types (nfc, nfc-static, nfc-dynamic) - Maintains backward compatibility with legacy NfcRenderingTemplate2024 Public API includes supportsNFC() to check NFC capability and renderToNfc() to generate NFC payload bytes from credentials. --- lib/nfcRenderer.js | 468 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 lib/nfcRenderer.js diff --git a/lib/nfcRenderer.js b/lib/nfcRenderer.js new file mode 100644 index 0000000..793fab1 --- /dev/null +++ b/lib/nfcRenderer.js @@ -0,0 +1,468 @@ +/** + * VC NFC Renderer Library + * Handles NFC rendering for verifiable credentials. + * Supports both static and dynamic rendering modes. + */ +import * as base58 from 'base58-universal'; +import * as base64url from 'base64url-universal'; + +const multibaseDecoders = new Map([ + ['u', base64url], + ['z', base58] +]); + +// ============ +// Public API +// ============ + +/** + * Check if a verifiable credential supports NFC rendering. + * + * @param {object} options - Options object. + * @param {object} options.credential - The verifiable credential. + * @returns {boolean} - 'true' if NFC is supported. + */ +export function supportsNFC({credential} = {}) { + try { + const renderMethod = _findNFCRenderMethod({credential}); + if(renderMethod !== null) { + return true; + } + // no NFC render method found + return false; + } catch(error) { + return false; + } +} + +/** + * Render a verifiable credential to NFC payload bytes. + * + * Supports both static (pre-encoded) and dynamic (runtime extraction) + * rendering modes based on the renderSuite value: + * - "nfc-static": Uses template/payload field. + * - "nfc-dynamic": Extracts data using renderProperty. + * - "nfc": Generic fallback - static takes priority if both exist. + * + * @param {object} options - Options object. + * @param {object} options.credential - The verifiable credential. + * @returns {Promise} Object with bytes property: {bytes: Uint8Array}. + */ +export async function renderToNfc({credential} = {}) { + // find NFC-compatible render method + const renderMethod = _findNFCRenderMethod({credential}); + + if(!renderMethod) { + throw new Error( + 'The verifiable credential does not support NFC rendering.' + ); + } + + // determining rendering mode and route to appropriate handler + const suite = _getRenderSuite({renderMethod}); + + if(!suite) { + throw new Error('Unable to determine render suite for NFC rendering.'); + } + + let bytes; + + switch(suite) { + case 'nfc-static': + bytes = await _renderStatic({renderMethod}); + break; + case 'nfc-dynamic': + bytes = await _renderDynamic({renderMethod, credential}); + break; + case 'nfc': + // try static first, fall back to dynamic if renderProperty exists + + // BEHAVIOR: Static rendering has priority over dynamic rendering. + // If BOTH template/payload AND renderProperty exist, static is used + // and renderProperty is ignored (edge case). + if(_hasStaticPayload({renderMethod})) { + bytes = await _renderStatic({renderMethod}); + } else if(renderMethod.renderProperty) { + // renderProperty exists, proceed with dynamic rendering + bytes = await _renderDynamic({renderMethod, credential}); + } else { + throw new Error( + 'NFC render method has neither payload nor renderProperty.' + ); + } + break; + default: + throw new Error(`Unsupported renderSuite: ${suite}`); + } + + // wrap in object for consistent return format + return {bytes}; +} + +// ======================== +// Render method detection +// ======================== + +/** + * Find the NFC-compatible render method in a verifiable credential. + * + * @private + * @param {object} options - Options object. + * @param {object} options.credential - The verifiable credential. + * @returns {object|null} The NFC render method or null. + */ +function _findNFCRenderMethod({credential} = {}) { + let renderMethods = credential?.renderMethod; + + if(!renderMethods) { + return null; + } + + // normalize to array for consistent handling + if(!Array.isArray(renderMethods)) { + renderMethods = [renderMethods]; + } + + // search for NFC-compatible render methods + for(const method of renderMethods) { + // check for W3C spec format with nfc renderSuite + if(method.type === 'TemplateRenderMethod') { + const suite = method.renderSuite?.toLowerCase(); + if(suite && suite.startsWith('nfc')) { + return method; + } + } + + // check for legacy format/existing codebase in + // bedrock-web-wallet/lib/helper.js file + if(method.type === 'NfcRenderingTemplate2024') { + return method; + } + } + + return null; +} + +/** + * Get the render suite with fallback for legacy formats. + * + * @private + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @returns {string} The render suite identifier. + */ +function _getRenderSuite({renderMethod} = {}) { + // use renderSuite if present + if(renderMethod.renderSuite) { + return renderMethod.renderSuite.toLowerCase(); + } + + // legacy format defaults to static + if(renderMethod.type === 'NfcRenderingTemplate2024') { + return 'nfc-static'; + } + + // generic fallback + return 'nfc'; +} + +/** + * Check if render method has a static payload. + * + * @private + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @returns {boolean} - 'true' if has template or payload field. + */ +function _hasStaticPayload({renderMethod} = {}) { + if(renderMethod.template || renderMethod.payload) { + return true; + } + return false; +} + +// ======================== +// Static rendering +// ======================== + +/** + * Render static NFC payload. + * + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @returns {Promise} - NFC payload as bytes. + */ +async function _renderStatic({renderMethod} = {}) { + + // get the payload from template or payload field + const encoded = renderMethod.template || renderMethod.payload; + + if(!encoded) { + throw new Error( + 'Static NFC render method has no template or payload field.' + ); + } + + if(typeof encoded !== 'string') { + throw new Error('Template or payload must be a string.'); + } + + // decoded based on format + if(encoded.startsWith('data:')) { + // data URI format + return _decodeDataUri({dataUri: encoded}); + } + if(encoded[0] === 'z' || encoded[0] === 'u') { + // multibase format + return _decodeMultibase({input: encoded}); + } + throw new Error('Unknown payload encoding format'); +} + +// ======================== +// Dynamic rendering +// ======================== + +/** + * Render dynamic NFC payload by extracting data from a verifiable + * credential. + * + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @param {object} options.credential - The verifiable credential. + * @returns {Promise} - NFC payload as bytes. + */ +async function _renderDynamic( + {renderMethod, credential} = {}) { + + // validate renderProperty exists + if(!renderMethod.renderProperty) { + throw new Error('Dynamic NFC rendering requires renderProperty.'); + } + + // normalize to array for consistent handling + const propertyPaths = Array.isArray(renderMethod.renderProperty) ? + renderMethod.renderProperty : [renderMethod.renderProperty]; + + if(propertyPaths.length === 0) { + throw new Error('renderProperty cannot be empty.'); + } + + // extract values from a verifiable credential using JSON pointers + const extractedValues = []; + + for(const path of propertyPaths) { + const value = _resolveJSONPointer({obj: credential, pointer: path}); + + if(value === undefined) { + throw new Error(`Property not found in credential: ${path}`); + } + + extractedValues.push({path, value}); + } + + // build the NFC payload from extracted values + return _buildDynamicPayload( + {extractedValues}); +} + +/** + * Build NFC payload from extracted credential values. + * + * @private + * @param {object} options - Options object. + * @param {Array} options.extractedValues - Extracted values with paths. + * @returns {Uint8Array} - NFC payload as bytes. + */ +function _buildDynamicPayload({extractedValues} = {}) { + + // simple concatenation of UTF-8 encoded values + const chunks = []; + + for(const item of extractedValues) { + const valueBytes = _encodeValue({value: item.value}); + chunks.push(valueBytes); + } + + // concatenate all chunks into single payload + const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const result = new Uint8Array(totalLength); + + let offset = 0; + for(const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result; +} + +/** + * Encode a value to bytes. + * + * @private + * @param {object} options - Options object. + * @param {*} options.value - The value to encode. + * @returns {Uint8Array} The encoded bytes. + */ +function _encodeValue({value} = {}) { + if(typeof value === 'string') { + // UTF-8 encode strings + return new TextEncoder().encode(value); + } + if(typeof value === 'number') { + // convert number to string then encode + return new TextEncoder().encode(String(value)); + } + if(typeof value === 'object') { + // JSON stringify objects + return new TextEncoder().encode(JSON.stringify(value)); + } + // fallback: convert to string + return new TextEncoder().encode(String(value)); +} + +// ======================== +// Decoding utilities +// ======================== + +/** + * Decode a data URI to bytes. + * + * @private + * @param {object} options - Options object. + * @param {string} options.dataUri - Data URI string. + * @returns {Uint8Array} Decoded bytes. + */ +function _decodeDataUri({dataUri} = {}) { + // parse data URI format: data:mime/type;encoding,data + const match = dataUri.match(/^data:([^;]+);([^,]+),(.*)$/); + + if(!match) { + throw new Error('Invalid data URI format.'); + } + + // const mimeType = match[1]; + const encoding = match[2]; + const data = match[3]; + + // decode based on encoding + if(encoding === 'base64') { + return _base64ToBytes({base64String: data}); + } + if(encoding === 'base64url') { + return base64url.decode(data); + } + throw new Error(`Unsupported data URI encoding: ${encoding}`); +} + +/** + * Decode multibase-encoded string. + * + * @private + * @param {object} options - Options object. + * @param {string} options.input - Multibase encoded string. + * @returns {Uint8Array} Decoded bytes. + */ +function _decodeMultibase({input} = {}) { + const header = input[0]; + const encodedData = input.slice(1); + + const decoder = multibaseDecoders.get(header); + if(!decoder) { + throw new Error(`Unsupported multibase header: ${header}`); + } + + return decoder.decode(encodedData); +} + +/** + * Decode standard base64 to bytes. + * + * @private + * @param {object} options - Options object. + * @param {string} options.base64String - Base64 encoded string. + * @returns {Uint8Array} Decoded bytes. + */ +function _base64ToBytes({base64String} = {}) { + // use atob in browser, Buffer in Node + if(typeof atob !== 'undefined') { + const binaryString = atob(base64String); + const bytes = new Uint8Array(binaryString.length); + for(let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + // Node.js environment + return Buffer.from(base64String, 'base64'); +} + +// ======================== +// JSON pointer utilities +// ======================== + +/** + * Resolve a JSON pointer in an object per RFC 6901. + * + * @private + * @param {object} options - Options object. + * @param {object} options.obj - The object to traverse. + * @param {string} options.pointer - JSON pointer string. + * @returns {*} The value at the pointer location or undefined. + */ +function _resolveJSONPointer({obj, pointer} = {}) { + // handle empty pointer (refers to entire document) + if(pointer === '' || pointer === '/') { + return obj; + } + + // remove leading slash + let path = pointer; + if(path.startsWith('/')) { + path = path.slice(1); + } + + // split into segments + const segments = path.split('/'); + + // traverse the object + let current = obj; + + for(const segment of segments) { + // decode special characters per RFC 6901: ~1 = /, ~0 = ~ + const decoded = segment + .replace(/~1/g, '/') + .replace(/~0/g, '~'); + + // handle array indices + if(Array.isArray(current)) { + const index = parseInt(decoded, 10); + if(isNaN(index) || index < 0 || index >= current.length) { + return undefined; + } + current = current[index]; + } else if(typeof current === 'object' && current !== null) { + current = current[decoded]; + } else { + return undefined; + } + + // return early if undefined + if(current === undefined) { + return undefined; + } + } + + return current; +} + +// ============ +// Exports +// ============ + +export default { + supportsNFC, + renderToNfc +}; From f8a9d94ea02abbb59deb4516df9f6f93725a8bf4 Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Tue, 11 Nov 2025 17:43:29 -0800 Subject: [PATCH 2/8] Add test suite for NFC renderer. Implements test suite covering NFC rendering functionality: - Tests supportsNFC() with various renderSuite configurations - Tests renderToNfc() static rendering with multibase, base64url, and data URIs - Tests renderToNfc() dynamic rendering with renderProperty extraction - Tests EAD credential handling with single/multiple field extraction - Tests legacy NfcRenderingTemplate2024 type compatibility - Tests error handling for invalid credentials and missing parameters - Includes real-world credential tests fetched from external sources Ensures compliance with W3C VC Rendering Methods specification. --- test/web/15-nfc-renderer.js | 1035 +++++++++++++++++++++++++++++++++++ 1 file changed, 1035 insertions(+) create mode 100644 test/web/15-nfc-renderer.js diff --git a/test/web/15-nfc-renderer.js b/test/web/15-nfc-renderer.js new file mode 100644 index 0000000..49ba3b4 --- /dev/null +++ b/test/web/15-nfc-renderer.js @@ -0,0 +1,1035 @@ +import * as webWallet from '@bedrock/web-wallet'; +// console.log(webWallet.nfcRenderer); +// console.log(typeof webWallet.nfcRenderer.supportsNFC); + +describe('NFC Renderer', function() { + describe('supportsNFC()', function() { + // Test to verify if a credential supports NFC rendering. + it('should return true for credential with nfc-static renderSuite', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } + ); + + it('should return true for credential with nfc-dynamic renderSuite', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'John Doe' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/name'] + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } + ); + + it('should return true for credential with generic nfc renderSuite', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } + ); + + it('should work with legacy NfcRenderingTemplate2024 using payload field', + async () => { + const credential = { + renderMethod: { + type: 'NfcRenderingTemplate2024', + // Using 'payload', not 'template' + payload: 'z2drAj5bAkJFsTPKmBvG3Z' + } + }; + const result = await webWallet.nfcRenderer.renderToNfc({credential}); + should.exist(result.bytes); + } + ); + + it('should return true for legacy NfcRenderingTemplate2024 type', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'NfcRenderingTemplate2024', + template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } + ); + + it('should return true for credential with renderMethod array', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: [ + { + type: 'SvgRenderingTemplate2023', + template: 'some-svg-data' + }, + { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' + } + ] + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } + ); + + it('should return false for credential without renderMethod', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(false); + } + ); + + it('should return false for credential with non-NFC renderMethod', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'SvgRenderingTemplate2023', + template: 'some-svg-data' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(false); + } + ); + }); + + describe('renderToNfc() - Static Rendering', function() { + it('should successfully render static NFC with multibase-encoded template', + async () => { + // Base58 encoded "Hello NFC" + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + template: 'z2drAj5bAkJFsTPKmBvG3Z' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + result.bytes.should.be.an.instanceof(Uint8Array); + result.bytes.length.should.be.greaterThan(0); + } + ); + + it('should successfully render static NFC with base64url-encoded payload', + async () => { + // Base64URL encoded "Test Data" + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + payload: 'uVGVzdCBEYXRh' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + result.bytes.should.be.an.instanceof(Uint8Array); + } + ); + + it('should successfully render static NFC with data URI format', + async () => { + // Base64 encoded "NFC Data" + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + template: 'data:application/octet-stream;base64,TkZDIERhdGE=' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + result.bytes.should.be.an.instanceof(Uint8Array); + // Verify decoded content + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('NFC Data'); + } + ); + + it('should use template field when both template and payload exist', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + // "Hello NFC" + template: 'z2drAj5bAkJFsTPKmBvG3Z', + // "Different" + payload: 'uRGlmZmVyZW50' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + } + ); + + it('should work with legacy NfcRenderingTemplate2024 type', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'NfcRenderingTemplate2024', + template: 'z2drAj5bAkJFsTPKmBvG3Z' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + } + ); + + it('should fail when static payload is missing', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static' + // No template or payload field + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('template or payload'); + } + ); + + it('should fail when payload encoding is invalid', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + template: 'xInvalidEncoding123' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('encoding format'); + } + ); + }); + + describe('renderToNfc() - Dynamic Rendering', function() { + it('should successfully render dynamic NFC with single renderProperty', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'Alice Smith' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/name'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + result.bytes.should.be.an.instanceof(Uint8Array); + // Verify content + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('Alice Smith'); + } + ); + + it('should successfully render dynamic NFC with multiple renderProperty', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + firstName: 'Alice', + lastName: 'Smith' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: [ + '/credentialSubject/firstName', + '/credentialSubject/lastName' + ] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + // Verify content (concatenated) + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('AliceSmith'); + } + ); + + it('should handle numeric values in dynamic rendering', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + age: 25 + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/age'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('25'); + + } + ); + + it('should handle object values in dynamic rendering', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + address: { + street: '123 Main St', + city: 'Boston' + } + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/address'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + // Should be JSON stringified + const decoded = new TextDecoder().decode(result.bytes); + const parsed = JSON.parse(decoded); + parsed.street.should.equal('123 Main St'); + parsed.city.should.equal('Boston'); + } + ); + + it('should handle array access in JSON pointer', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + skills: ['JavaScript', 'Python', 'Rust'] + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/skills/0'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('JavaScript'); + } + ); + + it('should handle special characters in JSON pointer', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + 'field/with~slash': 'test-value' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/field~1with~0slash'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('test-value'); + } + ); + + it('should fail when renderProperty is missing', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'Alice' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic' + // No renderProperty + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('renderProperty'); + } + ); + + it('should fail when renderProperty path does not exist', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'Alice' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/nonExistentField'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('Property not found'); + } + ); + + it('should fail when renderProperty is empty array', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: [] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('cannot be empty'); + } + ); + }); + + describe('renderToNfc() - Generic NFC Suite', function() { + it('should prioritize static rendering when both payload and ' + + 'renderProperty exist', async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'Alice' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + // "Hello NFC" + template: 'z2drAj5bAkJFsTPKmBvG3Z', + renderProperty: ['/credentialSubject/name'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + // Should use static rendering (template), not dynamic + const decoded = new TextDecoder().decode(result.bytes); + // If it was dynamic, it would be "Alice" + decoded.should.not.equal('Alice'); + }); + + it('should fallback to dynamic rendering when only renderProperty exists', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'Bob' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + renderProperty: ['/credentialSubject/name'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('Bob'); + } + ); + + it('should fail when neither payload nor renderProperty exist', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('neither payload nor renderProperty'); + } + ); + }); + + describe('renderToNfc() - Error Cases', function() { + it('should fail when credential has no renderMethod', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('does not support NFC rendering'); + } + ); + + it('should fail when renderSuite is unsupported', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'unsupported-suite', + template: 'some-data' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('does not support NFC rendering'); + } + ); + + it('should fail when credential parameter is missing', + async () => { + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + } + ); + }); + + describe('NFC Renderer - EAD Credential Tests (from URL)', function() { + let eadCredential; + + // Fetch the credential once before all tests + before(async function() { + // Increase timeout for network request + this.timeout(5000); + + try { + const response = await fetch( + 'https://gist.githubusercontent.com/gannan08/b03a8943c1ed1636a74e1f1966d24b7c/raw/fca19c491e2ab397d9c547e1858ba4531dd4e3bf/full-example-ead.json' + ); + + if(!response.ok) { + throw new Error(`Failed to fetch credential: ${response.status}`); + } + + eadCredential = await response.json(); + console.log('✓ EAD Credential loaded from URL'); + } catch(error) { + console.error('Failed to load EAD credential:', error); + // Skip all tests if credential can't be loaded + this.skip(); + } + }); + + describe('supportsNFC() - EAD from URL', function() { + it('should return false for EAD credential without renderMethod', + function() { + // Skip if credential wasn't loaded + if(!eadCredential) { + this.skip(); + } + + // Destructure to exclude renderMethod + // Create credential copy without renderMethod + const credentialWithoutRenderMethod = {...eadCredential}; + delete credentialWithoutRenderMethod.renderMethod; + + const result = webWallet.nfcRenderer.supportsNFC({ + credential: credentialWithoutRenderMethod + }); + + should.exist(result); + result.should.equal(false); + } + ); + + it('should return true for EAD credential with renderMethod', + function() { + if(!eadCredential) { + this.skip(); + } + + const result = webWallet.nfcRenderer.supportsNFC({ + credential: eadCredential + }); + + should.exist(result); + result.should.equal(true); + } + ); + + it('should return true when adding nfc-dynamic renderMethod', + function() { + if(!eadCredential) { + this.skip(); + } + + const credentialWithDynamic = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/givenName'] + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({ + credential: credentialWithDynamic + }); + + should.exist(result); + result.should.equal(true); + } + ); + + it('should return true when adding nfc-static renderMethod', + function() { + if(!eadCredential) { + this.skip(); + } + + const credential = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-static', + template: 'z2drAj5bAkJFsTPKmBvG3Z' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({ + credential + }); + + should.exist(result); + result.should.equal(true); + } + ); + }); + + describe('renderToNfc() - EAD Single Field Extraction', function() { + it('should extract givenName from EAD credential', + async function() { + if(!eadCredential) { + this.skip(); + } + + const credential = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/givenName'] + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('JOHN'); + } + ); + + it('should extract familyName from EAD credential', + async function() { + if(!eadCredential) { + this.skip(); + } + + const credential = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/familyName'] + } + }; + + const result = await webWallet.nfcRenderer.renderToNfc({credential}); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('SMITH'); + } + ); + + it('should extract full name (concatenated)', + async function() { + if(!eadCredential) { + this.skip(); + } + + const credential = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: [ + '/credentialSubject/givenName', + '/credentialSubject/additionalName', + '/credentialSubject/familyName' + ] + } + }; + + const result = await webWallet.nfcRenderer.renderToNfc({credential}); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('JOHNJACOBSMITH'); + } + ); + }); + + describe('renderToNfc() - EAD Image Data', function() { + it('should extract large image data URI', + async function() { + if(!eadCredential) { + this.skip(); + } + + const credential = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc-dynamic', + renderProperty: ['/credentialSubject/image'] + } + }; + + const result = await webWallet.nfcRenderer.renderToNfc({credential}); + + should.exist(result); + should.exist(result.bytes); + + const decoded = new TextDecoder().decode(result.bytes); + // Use regex to check starts with + decoded.should.match(/^data:image\/png;base64,/); + + // Verify it's the full large image (should be > 50KB) + result.bytes.length.should.be.greaterThan(50000); + } + ); + }); + + }); + +}); + From 7dd93f0dedeacfe1388f3406ecb8d1eef7e65429 Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Tue, 11 Nov 2025 17:44:59 -0800 Subject: [PATCH 3/8] Refactor NFC rendering to use new nfcRenderer library. Replaces legacy NFC rendering implementation in helper.js with calls to the new standardized nfcRenderer library functions: - Replace toNFCPayload() implementation with renderToNfc() call - Replace hasNFCPayload() implementation with supportsNFC() call - Remove unused base58 and base64url imports - Comment out legacy helper functions: _getNFCRenderingTemplate2024() and _decodeMultibase() - Comment out multibaseDecoders constant (now handled in nfcRenderer.js) This change consolidates NFC rendering logic into a single reusable library while maintaining backward compatibility with existing API. The new implementation supports both W3C spec formats and legacy NfcRenderingTemplate2024 type. --- lib/helpers.js | 126 +++++++++++++++++++++++++++---------------------- lib/index.js | 3 +- 2 files changed, 71 insertions(+), 58 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index cd48836..ba94e3b 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,8 +1,9 @@ /*! * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. */ -import * as base58 from 'base58-universal'; -import * as base64url from 'base64url-universal'; +// import * as base58 from 'base58-universal'; +// import * as base64url from 'base64url-universal'; +import {renderToNfc, supportsNFC} from './nfcRenderer.js'; import {config} from '@bedrock/web'; import { Ed25519VerificationKey2020 @@ -19,10 +20,10 @@ const supportedSignerTypes = new Map([ ] ]); -const multibaseDecoders = new Map([ - ['u', base64url], - ['z', base58], -]); +// const multibaseDecoders = new Map([ +// ['u', base64url], +// ['z', base58], +// ]); export async function createCapabilities({profileId, request}) { // TODO: validate `request` @@ -178,62 +179,73 @@ export async function openFirstPartyWindow(event) { export const prettify = obj => JSON.stringify(obj, null, 2); export async function toNFCPayload({credential}) { - const nfcRenderingTemplate2024 = _getNFCRenderingTemplate2024({credential}); - const bytes = await _decodeMultibase(nfcRenderingTemplate2024.payload); - return {bytes}; -} - -export function hasNFCPayload({credential}) { - try { - const nfcRenderingTemplate2024 = _getNFCRenderingTemplate2024({credential}); - if(!nfcRenderingTemplate2024) { - return false; - } - - return true; - } catch(e) { - return false; - } + return renderToNfc({credential}); } -function _getNFCRenderingTemplate2024({credential}) { - let {renderMethod} = credential; - if(!renderMethod) { - throw new Error('Credential does not contain "renderMethod".'); - } - - renderMethod = Array.isArray(renderMethod) ? renderMethod : [renderMethod]; - - let nfcRenderingTemplate2024 = null; - for(const rm of renderMethod) { - if(rm.type === 'NfcRenderingTemplate2024') { - nfcRenderingTemplate2024 = rm; - break; - } - continue; - } - - if(nfcRenderingTemplate2024 === null) { - throw new Error('Credential does not support "NfcRenderingTemplate2024".'); - } - - if(!nfcRenderingTemplate2024.payload) { - throw new Error('NfcRenderingTemplate2024 does not contain "payload".'); - } +// export async function toNFCPayload({credential}) { +// const nfcRenderingTemplate2024 = +// _getNFCRenderingTemplate2024({credential}); +// const bytes = await _decodeMultibase(nfcRenderingTemplate2024.payload); +// return {bytes}; +// } - return nfcRenderingTemplate2024; +export function hasNFCPayload({credential}) { + return supportsNFC({credential}); } -async function _decodeMultibase(input) { - const multibaseHeader = input[0]; - const decoder = multibaseDecoders.get(multibaseHeader); - if(!decoder) { - throw new Error(`Multibase header "${multibaseHeader}" not supported.`); - } - - const encodedStr = input.slice(1); - return decoder.decode(encodedStr); -} +// export function hasNFCPayload({credential}) { +// try { +// const nfcRenderingTemplate2024 = +// _getNFCRenderingTemplate2024({credential}); +// if(!nfcRenderingTemplate2024) { +// return false; +// } + +// return true; +// } catch(e) { +// return false; +// } +// } + +// function _getNFCRenderingTemplate2024({credential}) { +// let {renderMethod} = credential; +// if(!renderMethod) { +// throw new Error('Credential does not contain "renderMethod".'); +// } + +// renderMethod = Array.isArray(renderMethod) ? renderMethod : [renderMethod]; + +// let nfcRenderingTemplate2024 = null; +// for(const rm of renderMethod) { +// if(rm.type === 'NfcRenderingTemplate2024') { +// nfcRenderingTemplate2024 = rm; +// break; +// } +// continue; +// } + +// if(nfcRenderingTemplate2024 === null) { +// throw new Error( +// 'Credential does not support "NfcRenderingTemplate2024".'); +// } + +// if(!nfcRenderingTemplate2024.payload) { +// throw new Error('NfcRenderingTemplate2024 does not contain "payload".'); +// } + +// return nfcRenderingTemplate2024; +// } + +// async function _decodeMultibase(input) { +// const multibaseHeader = input[0]; +// const decoder = multibaseDecoders.get(multibaseHeader); +// if(!decoder) { +// throw new Error(`Multibase header "${multibaseHeader}" not supported.`); +// } + +// const encodedStr = input.slice(1); +// return decoder.decode(encodedStr); +// } async function _updateProfileAgentUser({ accessManager, profileAgent, profileAgentContent diff --git a/lib/index.js b/lib/index.js index eb97e37..bdb80d8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -19,13 +19,14 @@ import * as cryptoSuites from './cryptoSuites.js'; import * as exchanges from './exchanges/index.js'; import * as helpers from './helpers.js'; import * as inbox from './inbox.js'; +import * as nfcRenderer from './nfcRenderer.js'; import * as presentations from './presentations.js'; import * as users from './users.js'; import * as validator from './validator.js'; import * as zcap from './zcap.js'; export { ageCredentialHelpers, capabilities, cryptoSuites, exchanges, - helpers, inbox, presentations, users, validator, zcap + helpers, inbox, nfcRenderer, presentations, users, validator, zcap }; export { getCredentialStore, getProfileEdvClient, initialize, profileManager From 83386f155de3df6aef941f65e6862ae9047d373e Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Fri, 14 Nov 2025 19:10:16 -0800 Subject: [PATCH 4/8] Enforce strict field validation for NFC render methods. Implements field validation: - TemplateRenderMethod: requires 'template' field only (W3C spec) - NfcRenderingTemplate2024: requires 'payload' field only (legacy) - Rejects credentials with both fields present Updates _renderStatic() and _hasStaticPayload() functions with explicit field checking and clear error messages. Adds comprehensive validation tests and fixes existing tests to use correct fields for their respective render method types. --- lib/nfcRenderer.js | 62 +++++++++++++++++++++++----- test/web/15-nfc-renderer.js | 81 +++++++++++++++++++++++-------------- 2 files changed, 102 insertions(+), 41 deletions(-) diff --git a/lib/nfcRenderer.js b/lib/nfcRenderer.js index 793fab1..275e269 100644 --- a/lib/nfcRenderer.js +++ b/lib/nfcRenderer.js @@ -2,6 +2,10 @@ * VC NFC Renderer Library * Handles NFC rendering for verifiable credentials. * Supports both static and dynamic rendering modes. + * + * Field Requirements: + * - TemplateRenderMethod (W3C spec): MUST use "template" field. + * - NfcRenderingTemplate2024 (legacy): MUST use "payload" field. */ import * as base58 from 'base58-universal'; import * as base64url from 'base64url-universal'; @@ -40,7 +44,7 @@ export function supportsNFC({credential} = {}) { * * Supports both static (pre-encoded) and dynamic (runtime extraction) * rendering modes based on the renderSuite value: - * - "nfc-static": Uses template/payload field. + * - "nfc-static": Uses template (W3C spec) or payload (legacy) field. * - "nfc-dynamic": Extracts data using renderProperty. * - "nfc": Generic fallback - static takes priority if both exist. * @@ -172,12 +176,27 @@ function _getRenderSuite({renderMethod} = {}) { * @private * @param {object} options - Options object. * @param {object} options.renderMethod - The render method object. - * @returns {boolean} - 'true' if has template or payload field. + * @returns {boolean} - 'true' if has appropriate field for render method type. */ function _hasStaticPayload({renderMethod} = {}) { - if(renderMethod.template || renderMethod.payload) { - return true; + // enforce field usage based on render method type + if(renderMethod.type === 'NfcRenderingTemplate2024') { + // legacy format: check for 'payload' field + if(renderMethod && renderMethod.payload) { + return true; + } + return false; + } + if(renderMethod.type === 'TemplateRenderMethod') { + // W3C Spec format: check for 'template' field + if(renderMethod && renderMethod.template) { + return true; + } + return false; } + // if(renderMethod.template || renderMethod.payload) { + // return true; + // } return false; } @@ -194,13 +213,36 @@ function _hasStaticPayload({renderMethod} = {}) { */ async function _renderStatic({renderMethod} = {}) { - // get the payload from template or payload field - const encoded = renderMethod.template || renderMethod.payload; + // enforce field usage based on render method type + let encoded; - if(!encoded) { - throw new Error( - 'Static NFC render method has no template or payload field.' - ); + // get the payload from template or payload field + // const encoded = renderMethod.template || renderMethod.payload; + if(renderMethod.type === 'NfcRenderingTemplate2024') { + if(renderMethod.template && renderMethod.payload) { + throw new Error('NfcRenderingTemplate2024 should not have' + + ' both template and payload fields.' + ); + } + // legacy format: ONLY accept 'payload' field + encoded = renderMethod.payload; + if(!encoded) { + throw new Error('NfcRenderingTemplate2024 requires "payload" field.'); + } + } else if(renderMethod.type === 'TemplateRenderMethod') { + // W3C Spec format: ONLY accept 'template' field + if(renderMethod.template && renderMethod.payload) { + throw new Error('TemplateRenderMethod requires "template"' + + ' and should not have both fields.' + ); + } + encoded = renderMethod.template; + if(!encoded) { + throw new Error('TemplateRenderMethod requires "template" field.'); + } + } else { + // This should never happen given _findNFCRenderMethod() logic + throw new Error(`Unsupported render method type: ${renderMethod.type}`); } if(typeof encoded !== 'string') { diff --git a/test/web/15-nfc-renderer.js b/test/web/15-nfc-renderer.js index 49ba3b4..9f45253 100644 --- a/test/web/15-nfc-renderer.js +++ b/test/web/15-nfc-renderer.js @@ -63,20 +63,7 @@ describe('NFC Renderer', function() { } ); - it('should work with legacy NfcRenderingTemplate2024 using payload field', - async () => { - const credential = { - renderMethod: { - type: 'NfcRenderingTemplate2024', - // Using 'payload', not 'template' - payload: 'z2drAj5bAkJFsTPKmBvG3Z' - } - }; - const result = await webWallet.nfcRenderer.renderToNfc({credential}); - should.exist(result.bytes); - } - ); - + // Check one more time - return false and not work with template field it('should return true for legacy NfcRenderingTemplate2024 type', async () => { const credential = { @@ -84,7 +71,7 @@ describe('NFC Renderer', function() { type: ['VerifiableCredential'], renderMethod: { type: 'NfcRenderingTemplate2024', - template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' + payload: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' } }; @@ -182,7 +169,7 @@ describe('NFC Renderer', function() { } ); - it('should successfully render static NFC with base64url-encoded payload', + it('should successfully render static NFC with base64url-encoded template', async () => { // Base64URL encoded "Test Data" const credential = { @@ -191,7 +178,7 @@ describe('NFC Renderer', function() { renderMethod: { type: 'TemplateRenderMethod', renderSuite: 'nfc-static', - payload: 'uVGVzdCBEYXRh' + template: 'uVGVzdCBEYXRh' } }; @@ -240,8 +227,8 @@ describe('NFC Renderer', function() { decoded.should.equal('NFC Data'); } ); - - it('should use template field when both template and payload exist', + // Check one more time - as template and payload should never exist. + it('should fail when TemplateRenderMethod has both template and payload', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], @@ -264,19 +251,22 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); - should.exist(result.bytes); + should.exist(err); + should.not.exist(result); + err.message.should.contain( + 'TemplateRenderMethod requires "template" and should not have both'); } ); - it('should work with legacy NfcRenderingTemplate2024 type', + // template field instead of payload + it('should fail when NfcRenderingTemplate2024 uses template field', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], renderMethod: { type: 'NfcRenderingTemplate2024', + // wrong field - it should be payload template: 'z2drAj5bAkJFsTPKmBvG3Z' } }; @@ -289,13 +279,14 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); - should.exist(result.bytes); + should.exist(err); + should.not.exist(result); + err.message.should.contain( + 'NfcRenderingTemplate2024 requires "payload"'); } ); - - it('should fail when static payload is missing', + // Check one more time - no template field + it('should fail TemplateRenderMethod has no template field', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], @@ -303,7 +294,7 @@ describe('NFC Renderer', function() { renderMethod: { type: 'TemplateRenderMethod', renderSuite: 'nfc-static' - // No template or payload field + // No template field } }; @@ -317,7 +308,7 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('template or payload'); + err.message.should.contain('TemplateRenderMethod requires "template"'); } ); @@ -346,6 +337,34 @@ describe('NFC Renderer', function() { err.message.should.contain('encoding format'); } ); + + it('should work with legacy NfcRenderingTemplate2024 using payload field', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'NfcRenderingTemplate2024', + // Using 'payload', not 'template' + payload: 'z2drAj5bAkJFsTPKmBvG3Z' + } + }; + + let result; + let err; + + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(error) { + err = error; + } + + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + result.bytes.should.be.an.instanceof(Uint8Array); + } + ); }); describe('renderToNfc() - Dynamic Rendering', function() { @@ -711,7 +730,7 @@ describe('NFC Renderer', function() { } ); - it('should fail when neither payload nor renderProperty exist', + it('should fail when neither template nor renderProperty exist', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], From 4c3f1deae39472f790b40e323e8183701a66a899 Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Wed, 19 Nov 2025 20:35:33 -0800 Subject: [PATCH 5/8] Refactor NFC renderer to unified pipeline architecture. Replace separate static/dynamic rendering with unified pipeline where template is always required and renderProperty validates credential fields. Update implementation and tests to reflect single rendering flow: filter credential -> decode template -> return bytes. Changes: - Consolidate rendering functions into _decodeTemplateToBytes(). - Add _filterCredential() for renderProperty validation. - Remove obsolete static vs dynamic distinction. - Update all tests to match unified architecture. - Fix test encodings and add comprehensive error coverage. --- lib/nfcRenderer.js | 562 +++++++++++++++++------- test/web/15-nfc-renderer.js | 827 +++++++++++++++++++++++++++--------- 2 files changed, 1022 insertions(+), 367 deletions(-) diff --git a/lib/nfcRenderer.js b/lib/nfcRenderer.js index 275e269..d6aa1ad 100644 --- a/lib/nfcRenderer.js +++ b/lib/nfcRenderer.js @@ -42,18 +42,24 @@ export function supportsNFC({credential} = {}) { /** * Render a verifiable credential to NFC payload bytes. * - * Supports both static (pre-encoded) and dynamic (runtime extraction) - * rendering modes based on the renderSuite value: - * - "nfc-static": Uses template (W3C spec) or payload (legacy) field. - * - "nfc-dynamic": Extracts data using renderProperty. - * - "nfc": Generic fallback - static takes priority if both exist. + * Architecture: + * + * 1. Filter: Use renderProperty to extract specific fields + * (optional, for transparency). + * 2. Render: Pass template and filtered data to NFC rendering engine. + * 3. Output: Return decoded bytes from template. + * + * Template Requirement: + * - All NFC rendering requires a template field containing pre-encoded payload. + * - TemplateRenderMethod uses 'template' field (W3C spec). + * - NfcRenderingTemplate2024 uses 'payload' field (legacy). * * @param {object} options - Options object. * @param {object} options.credential - The verifiable credential. * @returns {Promise} Object with bytes property: {bytes: Uint8Array}. */ export async function renderToNfc({credential} = {}) { - // find NFC-compatible render method + // finc NFC-compatible render method const renderMethod = _findNFCRenderMethod({credential}); if(!renderMethod) { @@ -62,53 +68,173 @@ export async function renderToNfc({credential} = {}) { ); } - // determining rendering mode and route to appropriate handler - const suite = _getRenderSuite({renderMethod}); - - if(!suite) { - throw new Error('Unable to determine render suite for NFC rendering.'); + // require template/payload field + if(!_hasTemplate({renderMethod})) { + throw new Error( + 'NFC rendering requires a template field. ' + + 'The template should contain the pre-encoded NFC payload.' + ); } - let bytes; - - switch(suite) { - case 'nfc-static': - bytes = await _renderStatic({renderMethod}); - break; - case 'nfc-dynamic': - bytes = await _renderDynamic({renderMethod, credential}); - break; - case 'nfc': - // try static first, fall back to dynamic if renderProperty exists - - // BEHAVIOR: Static rendering has priority over dynamic rendering. - // If BOTH template/payload AND renderProperty exist, static is used - // and renderProperty is ignored (edge case). - if(_hasStaticPayload({renderMethod})) { - bytes = await _renderStatic({renderMethod}); - } else if(renderMethod.renderProperty) { - // renderProperty exists, proceed with dynamic rendering - bytes = await _renderDynamic({renderMethod, credential}); - } else { - throw new Error( - 'NFC render method has neither payload nor renderProperty.' - ); - } - break; - default: - throw new Error(`Unsupported renderSuite: ${suite}`); + // Step 1: Filter credential if renderProperty exists + let filteredData = null; + if(renderMethod.renderProperty && renderMethod.renderProperty.length > 0) { + filteredData = _filterCredential({credential, renderMethod}); } - // wrap in object for consistent return format + // Step 2: Pass both template and filteredData to rendering engine + const bytes = await _decodeTemplateToBytes({renderMethod, filteredData}); + + // Wrap in object for consistent return format return {bytes}; } +// TODO: Delete later. +/** + * Use renderToNFC instead renderToNfc_V0 function. + * Render a verifiable credential to NFC payload bytes. + * + * Supports both static (pre-encoded) and dynamic (runtime extraction) + * rendering modes based on the renderSuite value: + * - "nfc-static": Uses template (W3C spec) or payload (legacy) field. + * - "nfc-dynamic": Extracts data using renderProperty. + * - "nfc": Generic fallback - static takes priority if both exist. + * + * @param {object} options - Options object. + * @param {object} options.credential - The verifiable credential. + * @returns {Promise} Object with bytes property: {bytes: Uint8Array}. + */ +// export async function renderToNfc_V0({credential} = {}) { +// // find NFC-compatible render method +// const renderMethod = _findNFCRenderMethod({credential}); + +// if(!renderMethod) { +// throw new Error( +// 'The verifiable credential does not support NFC rendering.' +// ); +// } + +// // determining rendering mode and route to appropriate handler +// const suite = _getRenderSuite({renderMethod}); + +// if(!suite) { +// throw new Error('Unable to determine render suite for NFC rendering.'); +// } + +// let bytes; + +// switch(suite) { +// case 'nfc-static': +// bytes = await _renderStatic({renderMethod}); +// break; +// case 'nfc-dynamic': +// bytes = await _renderDynamic({renderMethod, credential}); +// break; +// case 'nfc': +// // try static first, fall back to dynamic if renderProperty exists + +// // BEHAVIOR: Static rendering has priority over dynamic rendering. +// // If BOTH template/payload AND renderProperty exist, static is used +// // and renderProperty is ignored (edge case). +// if(_hasStaticPayload({renderMethod})) { +// bytes = await _renderStatic({renderMethod}); +// } else if(renderMethod.renderProperty) { +// // renderProperty exists, proceed with dynamic rendering +// bytes = await _renderDynamic({renderMethod, credential}); +// } else { +// throw new Error( +// 'NFC render method has neither payload nor renderProperty.' +// ); +// } +// break; +// default: +// throw new Error(`Unsupported renderSuite: ${suite}`); +// } + +// // wrap in object for consistent return format +// return {bytes}; +// } + // ======================== // Render method detection // ======================== +/** + * Check if render method has a template field. + * + * Note: Template field name varies by type: + * - TemplateRenderMethod: uses 'template' field (W3C spec). + * - NfcRenderingTemplate2024: uses 'payload' field (legacy). + * + * @private + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @returns {boolean} - 'true' if template or payload field exists. + */ +function _hasTemplate({renderMethod} = {}) { + // enforce field usage based on render method type + if(renderMethod.type === 'TemplateRenderMethod') { + // W3C Spec format: check for 'template' field + if(renderMethod && renderMethod.template) { + return true; + } + return false; + } + + if(renderMethod.type === 'NfcRenderingTemplate2024') { + // legacy format: check for 'payload' field + if(renderMethod && renderMethod.payload) { + return true; + } + return false; + } + + return false; +} + +/** + * Filter credential data using renderProperty. + * Extracts only the fields specified in renderProperty + * for transparency. + * + * @private + * @param {object} options - Options object. + * @param {object} options.credential - The verifiable credential. + * @param {object} options.renderMethod - The render method object. + * @returns {object} - Filtered data object with extracted fields. + */ +function _filterCredential({credential, renderMethod} = {}) { + const {renderProperty} = renderMethod; + + // check if renderProperty exists and is not empty + if(!renderProperty || renderProperty.length == 0) { + return null; + } + + const filteredData = {}; + + // extract each field specified in renderProperty + for(const pointer of renderProperty) { + const value = _resolveJSONPointer({obj: credential, pointer}); + + // ensure property exists in credential + if(value === undefined) { + throw new Error(`Property not found in credential: ${pointer}`); + } + + // extract field name form pointer for key + // e.g., "/credentialSubject/name" -> "name" + const fieldName = pointer.split('/').pop(); + filteredData[fieldName] = value; + } + + return filteredData; +} + /** * Find the NFC-compatible render method in a verifiable credential. + * Checks modern format (TemplateRenderMethod) first, then legacy + * format (NfcRenderingTemplate2024). * * @private * @param {object} options - Options object. @@ -147,6 +273,7 @@ function _findNFCRenderMethod({credential} = {}) { return null; } +// TODO: Delete later. /** * Get the render suite with fallback for legacy formats. * @@ -155,21 +282,22 @@ function _findNFCRenderMethod({credential} = {}) { * @param {object} options.renderMethod - The render method object. * @returns {string} The render suite identifier. */ -function _getRenderSuite({renderMethod} = {}) { - // use renderSuite if present - if(renderMethod.renderSuite) { - return renderMethod.renderSuite.toLowerCase(); - } - - // legacy format defaults to static - if(renderMethod.type === 'NfcRenderingTemplate2024') { - return 'nfc-static'; - } - - // generic fallback - return 'nfc'; -} - +// function _getRenderSuite({renderMethod} = {}) { +// // use renderSuite if present +// if(renderMethod.renderSuite) { +// return renderMethod.renderSuite.toLowerCase(); +// } + +// // legacy format defaults to static +// if(renderMethod.type === 'NfcRenderingTemplate2024') { +// return 'nfc-static'; +// } + +// // generic fallback +// return 'nfc'; +// } + +// TODO: Delete later. /** * Check if render method has a static payload. * @@ -178,93 +306,194 @@ function _getRenderSuite({renderMethod} = {}) { * @param {object} options.renderMethod - The render method object. * @returns {boolean} - 'true' if has appropriate field for render method type. */ -function _hasStaticPayload({renderMethod} = {}) { - // enforce field usage based on render method type - if(renderMethod.type === 'NfcRenderingTemplate2024') { - // legacy format: check for 'payload' field - if(renderMethod && renderMethod.payload) { - return true; - } - return false; - } - if(renderMethod.type === 'TemplateRenderMethod') { - // W3C Spec format: check for 'template' field - if(renderMethod && renderMethod.template) { - return true; - } - return false; - } - // if(renderMethod.template || renderMethod.payload) { - // return true; - // } - return false; -} +// function _hasStaticPayload({renderMethod} = {}) { +// // enforce field usage based on render method type +// if(renderMethod.type === 'NfcRenderingTemplate2024') { +// // legacy format: check for 'payload' field +// if(renderMethod && renderMethod.payload) { +// return true; +// } +// return false; +// } +// if(renderMethod.type === 'TemplateRenderMethod') { +// // W3C Spec format: check for 'template' field +// if(renderMethod && renderMethod.template) { +// return true; +// } +// return false; +// } +// // if(renderMethod.template || renderMethod.payload) { +// // return true; +// // } +// return false; +// } // ======================== -// Static rendering +// NFC rendering engine // ======================== /** - * Render static NFC payload. + * Extract and validate template from render method. + * Enforces strict field usage based on render method type. * + * @private * @param {object} options - Options object. * @param {object} options.renderMethod - The render method object. - * @returns {Promise} - NFC payload as bytes. + * @returns {string} - Encoded template string. + * @throws {Error} - If validation fails. */ -async function _renderStatic({renderMethod} = {}) { - - // enforce field usage based on render method type +function _extractTemplate({renderMethod} = {}) { let encoded; - // get the payload from template or payload field - // const encoded = renderMethod.template || renderMethod.payload; - if(renderMethod.type === 'NfcRenderingTemplate2024') { + // check W3C spec format first + if(renderMethod.type === 'TemplateRenderMethod') { + // validate: should not have both fields if(renderMethod.template && renderMethod.payload) { - throw new Error('NfcRenderingTemplate2024 should not have' + - ' both template and payload fields.' + throw new Error( + 'TemplateRenderMethod requires "template". ' + + 'It should not have both fields.' ); } - // legacy format: ONLY accept 'payload' field - encoded = renderMethod.payload; + + encoded = renderMethod.template; if(!encoded) { - throw new Error('NfcRenderingTemplate2024 requires "payload" field.'); + throw new Error('TemplateRenderMethod requires "template" field.'); } - } else if(renderMethod.type === 'TemplateRenderMethod') { - // W3C Spec format: ONLY accept 'template' field + // check legacy format + } else if(renderMethod.type === 'NfcRenderingTemplate2024') { + // validate: should not have both fields if(renderMethod.template && renderMethod.payload) { - throw new Error('TemplateRenderMethod requires "template"' + - ' and should not have both fields.' + throw new Error( + 'NfcRenderingTemplate2024 should not have both template ' + + 'and payload fields.' ); } - encoded = renderMethod.template; + encoded = renderMethod.payload; if(!encoded) { - throw new Error('TemplateRenderMethod requires "template" field.'); + throw new Error('NfcRenderingTemplate2024 requires "payload" field.'); } } else { - // This should never happen given _findNFCRenderMethod() logic throw new Error(`Unsupported render method type: ${renderMethod.type}`); } + return encoded; +} + +/** + * Decode template to NFC payload bytes from template. + * Extract, validates, and decodes the template field. + * + * @private + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @param {object} options.filteredData - Filtered credential data + * (may be null). + * @returns {Promise} - NFC payload as bytes. + */ +// eslint-disable-next-line no-unused-vars +async function _decodeTemplateToBytes({renderMethod, filteredData} = {}) { + // Note: filteredData is reserved for future template + // processing with variables. Currently not used - + // as templates contain complete binary payloads. + + // extract and validate template/payload field + const encoded = _extractTemplate({renderMethod}); + + // validate template is a string if(typeof encoded !== 'string') { throw new Error('Template or payload must be a string.'); } - // decoded based on format + // Rendering: Decode the template to bytes + const bytes = await _decodeTemplate({encoded}); + + return bytes; +} + +async function _decodeTemplate({encoded} = {}) { + // data URI format if(encoded.startsWith('data:')) { - // data URI format return _decodeDataUri({dataUri: encoded}); } + + // multibase format (base58 'z' or base64url 'u') if(encoded[0] === 'z' || encoded[0] === 'u') { - // multibase format return _decodeMultibase({input: encoded}); } - throw new Error('Unknown payload encoding format'); + + throw new Error( + 'Unknown template encoding format. ' + + 'Supported formats: data URI (data:...) or multibase (z..., u...)' + ); } +// ======================== +// Static rendering +// ======================== + +// TODO: Delete later. +/** + * Render static NFC payload. + * + * @param {object} options - Options object. + * @param {object} options.renderMethod - The render method object. + * @returns {Promise} - NFC payload as bytes. + */ +// async function _renderStatic({renderMethod} = {}) { + +// // enforce field usage based on render method type +// let encoded; + +// // get the payload from template or payload field +// // const encoded = renderMethod.template || renderMethod.payload; +// if(renderMethod.type === 'NfcRenderingTemplate2024') { +// if(renderMethod.template && renderMethod.payload) { +// throw new Error('NfcRenderingTemplate2024 should not have' + +// ' both template and payload fields.' +// ); +// } +// // legacy format: ONLY accept 'payload' field +// encoded = renderMethod.payload; +// if(!encoded) { +// throw new Error('NfcRenderingTemplate2024 requires "payload" field.'); +// } +// } else if(renderMethod.type === 'TemplateRenderMethod') { +// // W3C Spec format: ONLY accept 'template' field +// if(renderMethod.template && renderMethod.payload) { +// throw new Error('TemplateRenderMethod requires "template"' + +// ' and should not have both fields.' +// ); +// } +// encoded = renderMethod.template; +// if(!encoded) { +// throw new Error('TemplateRenderMethod requires "template" field.'); +// } +// } else { +// // This should never happen given _findNFCRenderMethod() logic +// throw new Error(`Unsupported render method type: ${renderMethod.type}`); +// } + +// if(typeof encoded !== 'string') { +// throw new Error('Template or payload must be a string.'); +// } + +// // decoded based on format +// if(encoded.startsWith('data:')) { +// // data URI format +// return _decodeDataUri({dataUri: encoded}); +// } +// if(encoded[0] === 'z' || encoded[0] === 'u') { +// // multibase format +// return _decodeMultibase({input: encoded}); +// } +// throw new Error('Unknown payload encoding format'); +// } + // ======================== // Dynamic rendering // ======================== +// TODO: Delete later /** * Render dynamic NFC payload by extracting data from a verifiable * credential. @@ -274,40 +503,41 @@ async function _renderStatic({renderMethod} = {}) { * @param {object} options.credential - The verifiable credential. * @returns {Promise} - NFC payload as bytes. */ -async function _renderDynamic( - {renderMethod, credential} = {}) { +// async function _renderDynamic( +// {renderMethod, credential} = {}) { - // validate renderProperty exists - if(!renderMethod.renderProperty) { - throw new Error('Dynamic NFC rendering requires renderProperty.'); - } +// // validate renderProperty exists +// if(!renderMethod.renderProperty) { +// throw new Error('Dynamic NFC rendering requires renderProperty.'); +// } - // normalize to array for consistent handling - const propertyPaths = Array.isArray(renderMethod.renderProperty) ? - renderMethod.renderProperty : [renderMethod.renderProperty]; +// // normalize to array for consistent handling +// const propertyPaths = Array.isArray(renderMethod.renderProperty) ? +// renderMethod.renderProperty : [renderMethod.renderProperty]; - if(propertyPaths.length === 0) { - throw new Error('renderProperty cannot be empty.'); - } +// if(propertyPaths.length === 0) { +// throw new Error('renderProperty cannot be empty.'); +// } - // extract values from a verifiable credential using JSON pointers - const extractedValues = []; +// // extract values from a verifiable credential using JSON pointers +// const extractedValues = []; - for(const path of propertyPaths) { - const value = _resolveJSONPointer({obj: credential, pointer: path}); +// for(const path of propertyPaths) { +// const value = _resolveJSONPointer({obj: credential, pointer: path}); - if(value === undefined) { - throw new Error(`Property not found in credential: ${path}`); - } +// if(value === undefined) { +// throw new Error(`Property not found in credential: ${path}`); +// } - extractedValues.push({path, value}); - } +// extractedValues.push({path, value}); +// } - // build the NFC payload from extracted values - return _buildDynamicPayload( - {extractedValues}); -} +// // build the NFC payload from extracted values +// return _buildDynamicPayload( +// {extractedValues}); +// } +// TODO: Delete later. /** * Build NFC payload from extracted credential values. * @@ -316,29 +546,30 @@ async function _renderDynamic( * @param {Array} options.extractedValues - Extracted values with paths. * @returns {Uint8Array} - NFC payload as bytes. */ -function _buildDynamicPayload({extractedValues} = {}) { +// function _buildDynamicPayload({extractedValues} = {}) { - // simple concatenation of UTF-8 encoded values - const chunks = []; +// // simple concatenation of UTF-8 encoded values +// const chunks = []; - for(const item of extractedValues) { - const valueBytes = _encodeValue({value: item.value}); - chunks.push(valueBytes); - } +// for(const item of extractedValues) { +// const valueBytes = _encodeValue({value: item.value}); +// chunks.push(valueBytes); +// } - // concatenate all chunks into single payload - const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); - const result = new Uint8Array(totalLength); +// // concatenate all chunks into single payload +// const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); +// const result = new Uint8Array(totalLength); - let offset = 0; - for(const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; - } +// let offset = 0; +// for(const chunk of chunks) { +// result.set(chunk, offset); +// offset += chunk.length; +// } - return result; -} +// return result; +// } +// TODO: Delete later. /** * Encode a value to bytes. * @@ -347,22 +578,22 @@ function _buildDynamicPayload({extractedValues} = {}) { * @param {*} options.value - The value to encode. * @returns {Uint8Array} The encoded bytes. */ -function _encodeValue({value} = {}) { - if(typeof value === 'string') { - // UTF-8 encode strings - return new TextEncoder().encode(value); - } - if(typeof value === 'number') { - // convert number to string then encode - return new TextEncoder().encode(String(value)); - } - if(typeof value === 'object') { - // JSON stringify objects - return new TextEncoder().encode(JSON.stringify(value)); - } - // fallback: convert to string - return new TextEncoder().encode(String(value)); -} +// function _encodeValue({value} = {}) { +// if(typeof value === 'string') { +// // UTF-8 encode strings +// return new TextEncoder().encode(value); +// } +// if(typeof value === 'number') { +// // convert number to string then encode +// return new TextEncoder().encode(String(value)); +// } +// if(typeof value === 'object') { +// // JSON stringify objects +// return new TextEncoder().encode(JSON.stringify(value)); +// } +// // fallback: convert to string +// return new TextEncoder().encode(String(value)); +// } // ======================== // Decoding utilities @@ -370,11 +601,13 @@ function _encodeValue({value} = {}) { /** * Decode a data URI to bytes. + * Validates media type is application/octet-stream.. * * @private * @param {object} options - Options object. * @param {string} options.dataUri - Data URI string. * @returns {Uint8Array} Decoded bytes. + * @throws {Error} If data URI is invalid or has wrong media type. */ function _decodeDataUri({dataUri} = {}) { // parse data URI format: data:mime/type;encoding,data @@ -384,10 +617,19 @@ function _decodeDataUri({dataUri} = {}) { throw new Error('Invalid data URI format.'); } - // const mimeType = match[1]; + const mimeType = match[1]; const encoding = match[2]; const data = match[3]; + // validate media type is application/octet-stream + if(mimeType !== 'application/octet-stream') { + throw new Error( + 'Invalid data URI media type. ' + + 'NFC templates must use "application/octet-stream" media type. ' + + `Found: "${mimeType}"` + ); + } + // decode based on encoding if(encoding === 'base64') { return _base64ToBytes({base64String: data}); diff --git a/test/web/15-nfc-renderer.js b/test/web/15-nfc-renderer.js index 9f45253..4c43808 100644 --- a/test/web/15-nfc-renderer.js +++ b/test/web/15-nfc-renderer.js @@ -5,14 +5,14 @@ import * as webWallet from '@bedrock/web-wallet'; describe('NFC Renderer', function() { describe('supportsNFC()', function() { // Test to verify if a credential supports NFC rendering. - it('should return true for credential with nfc-static renderSuite', + it('should return true for credential with nfc renderSuite and template', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' } }; @@ -23,26 +23,30 @@ describe('NFC Renderer', function() { } ); - it('should return true for credential with nfc-dynamic renderSuite', - async () => { - const credential = { - '@context': ['https://www.w3.org/ns/credentials/v2'], - type: ['VerifiableCredential'], - credentialSubject: { - id: 'did:example:123', - name: 'John Doe' - }, - renderMethod: { - type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/name'] - } - }; + it('should return true even when template is missing ' + + '(detection, not validation', + async () => { + // Note: supportsNFC() only detects NFC capability, it doesn't validate. + // This credential will fail in renderToNfc() due to missing template. + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: { + id: 'did:example:123', + name: 'John Doe' + }, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + renderProperty: ['/credentialSubject/name'] + // missing template - will fail in renderToNfc() + } + }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); - should.exist(result); - result.should.equal(true); - } + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } ); it('should return true for credential with generic nfc renderSuite', @@ -63,7 +67,7 @@ describe('NFC Renderer', function() { } ); - // Check one more time - return false and not work with template field + // Legacy format uses 'payload' field instead of 'template' it('should return true for legacy NfcRenderingTemplate2024 type', async () => { const credential = { @@ -93,7 +97,7 @@ describe('NFC Renderer', function() { }, { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' } ] @@ -137,18 +141,37 @@ describe('NFC Renderer', function() { result.should.equal(false); } ); + + it('should detect NFC renderSuite case-insensitively', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + // uppercase + renderSuite: 'NFC', + template: 'z6Mkf5rGMoatrSj1f4CyvuHBeXJELe9RPdzo2rJQ' + } + }; + + const result = webWallet.nfcRenderer.supportsNFC({credential}); + should.exist(result); + result.should.equal(true); + } + ); }); - describe('renderToNfc() - Static Rendering', function() { + describe('renderToNfc() - Template Decoding', function() { it('should successfully render static NFC with multibase-encoded template', async () => { - // Base58 encoded "Hello NFC" + // Base58 multibase encoded "Hello NFC" (z = base58btc prefix) const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', template: 'z2drAj5bAkJFsTPKmBvG3Z' } }; @@ -177,7 +200,7 @@ describe('NFC Renderer', function() { type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', template: 'uVGVzdCBEYXRh' } }; @@ -199,13 +222,13 @@ describe('NFC Renderer', function() { it('should successfully render static NFC with data URI format', async () => { - // Base64 encoded "NFC Data" + // Data URI with base64 encoded "NFC Data" const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', template: 'data:application/octet-stream;base64,TkZDIERhdGE=' } }; @@ -227,7 +250,8 @@ describe('NFC Renderer', function() { decoded.should.equal('NFC Data'); } ); - // Check one more time - as template and payload should never exist. + + // Field validation: TemplateRenderMethod uses 'template', not 'payload' it('should fail when TemplateRenderMethod has both template and payload', async () => { const credential = { @@ -235,7 +259,7 @@ describe('NFC Renderer', function() { type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', // "Hello NFC" template: 'z2drAj5bAkJFsTPKmBvG3Z', // "Different" @@ -253,12 +277,11 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain( - 'TemplateRenderMethod requires "template" and should not have both'); + err.message.should.contain('template'); } ); - // template field instead of payload + // Field validation: NfcRenderingTemplate2024 uses 'payload', not 'template' it('should fail when NfcRenderingTemplate2024 uses template field', async () => { const credential = { @@ -281,11 +304,11 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain( - 'NfcRenderingTemplate2024 requires "payload"'); + err.message.should.contain('payload'); } ); - // Check one more time - no template field + + // Template is required for all NFC rendering it('should fail TemplateRenderMethod has no template field', async () => { const credential = { @@ -293,7 +316,7 @@ describe('NFC Renderer', function() { type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static' + renderSuite: 'nfc' // No template field } }; @@ -308,18 +331,18 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('TemplateRenderMethod requires "template"'); + err.message.should.contain('template'); } ); - it('should fail when payload encoding is invalid', + it('should fail when template encoding is invalid', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', + renderSuite: 'nfc', template: 'xInvalidEncoding123' } }; @@ -365,22 +388,24 @@ describe('NFC Renderer', function() { result.bytes.should.be.an.instanceof(Uint8Array); } ); - }); - describe('renderToNfc() - Dynamic Rendering', function() { - it('should successfully render dynamic NFC with single renderProperty', + it('should decode template even when renderProperty is present', async () => { + // Template contains "Hello NFC" + // renderProperty indicates what fields are disclosed (for transparency) const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { - id: 'did:example:123', - name: 'Alice Smith' + greeting: 'Hello' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/name'] + renderSuite: 'nfc', + // "Hello NFC" as base64 in data URI format + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', + // For transparency + renderProperty: ['/credentialSubject/greeting'] } }; @@ -395,30 +420,28 @@ describe('NFC Renderer', function() { should.not.exist(err); should.exist(result); should.exist(result.bytes); - result.bytes.should.be.an.instanceof(Uint8Array); - // Verify content + + // Should decode template, renderProperty is for transparency only const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('Alice Smith'); + decoded.should.equal('Hello NFC'); } ); - it('should successfully render dynamic NFC with multiple renderProperty', + it('should fail when renderProperty references non-existent field', async () => { + // Template is valid, but renderProperty validation fails const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { - id: 'did:example:123', - firstName: 'Alice', - lastName: 'Smith' + name: 'Alice' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: [ - '/credentialSubject/firstName', - '/credentialSubject/lastName' - ] + renderSuite: 'nfc', + template: 'z2drAj5bAkJFsTPKmBvG3Z', + // Doesn't exist! + renderProperty: ['/credentialSubject/nonExistent'] } }; @@ -430,28 +453,28 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); - should.exist(result.bytes); - // Verify content (concatenated) - const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('AliceSmith'); + should.exist(err); + should.not.exist(result); + err.message.should.contain('Property not found'); } ); + }); - it('should handle numeric values in dynamic rendering', + describe('renderToNfc() - renderProperty Validation', function() { + it('should fail when only renderProperty exists without template', async () => { + // In unified architecture, template is always required const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { id: 'did:example:123', - age: 25 + name: 'Alice Smith' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/age'] + renderSuite: 'nfc', + renderProperty: ['/credentialSubject/name'] } }; @@ -463,31 +486,32 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); - should.exist(result.bytes); - const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('25'); - + should.exist(err); + should.not.exist(result); + err.message.should.contain('template'); } ); - it('should handle object values in dynamic rendering', + it('should validate renderProperty field exists before decoding template', async () => { + // renderProperty validates credential has the field + // Then template is decoded (not the credential field!) const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { id: 'did:example:123', - address: { - street: '123 Main St', - city: 'Boston' - } + name: 'Alice Smith' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/address'] + renderSuite: 'nfc', + // "Hello NFC" encoded + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', + // Validates field exists + renderProperty: [ + '/credentialSubject/name', + ] } }; @@ -502,27 +526,168 @@ describe('NFC Renderer', function() { should.not.exist(err); should.exist(result); should.exist(result.bytes); - // Should be JSON stringified + // Should decode template, NOT extract "Alice Smith" const decoded = new TextDecoder().decode(result.bytes); - const parsed = JSON.parse(decoded); - parsed.street.should.equal('123 Main St'); - parsed.city.should.equal('Boston'); + decoded.should.equal('Hello NFC'); + decoded.should.not.equal('Alice Smith'); } ); - it('should handle array access in JSON pointer', + // TODO: Delete later + // it('should handle numeric values in dynamic rendering', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123', + // age: 25 + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc-dynamic', + // renderProperty: ['/credentialSubject/age'] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.not.exist(err); + // should.exist(result); + // should.exist(result.bytes); + // const decoded = new TextDecoder().decode(result.bytes); + // decoded.should.equal('25'); + + // } + // ); + + // TODO: Delete later + // it('should handle object values in dynamic rendering', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123', + // address: { + // street: '123 Main St', + // city: 'Boston' + // } + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc-dynamic', + // renderProperty: ['/credentialSubject/address'] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.not.exist(err); + // should.exist(result); + // should.exist(result.bytes); + // // Should be JSON stringified + // const decoded = new TextDecoder().decode(result.bytes); + // const parsed = JSON.parse(decoded); + // parsed.street.should.equal('123 Main St'); + // parsed.city.should.equal('Boston'); + // } + // ); + + // TODO: Delete later + // it('should handle array access in JSON pointer', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123', + // skills: ['JavaScript', 'Python', 'Rust'] + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc-dynamic', + // renderProperty: ['/credentialSubject/skills/0'] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.not.exist(err); + // should.exist(result); + // should.exist(result.bytes); + // const decoded = new TextDecoder().decode(result.bytes); + // decoded.should.equal('JavaScript'); + // } + // ); + + // TODO: Delete later + // it('should handle special characters in JSON pointer', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123', + // 'field/with~slash': 'test-value' + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc-dynamic', + // renderProperty: ['/credentialSubject/field~1with~0slash'] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.not.exist(err); + // should.exist(result); + // should.exist(result.bytes); + // const decoded = new TextDecoder().decode(result.bytes); + // decoded.should.equal('test-value'); + // } + // ); + + it('should succeed when renderProperty is missing but template exists', async () => { + // renderProperty is optional - template is what matters const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { id: 'did:example:123', - skills: ['JavaScript', 'Python', 'Rust'] + name: 'Alice' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/skills/0'] + renderSuite: 'nfc', + // "Hello NFC" + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD' + // No renderProperty } }; @@ -538,23 +703,26 @@ describe('NFC Renderer', function() { should.exist(result); should.exist(result.bytes); const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('JavaScript'); + decoded.should.equal('Hello NFC'); } ); - it('should handle special characters in JSON pointer', + it('should fail when renderProperty references non-existent field', async () => { + // Even though template is valid, renderProperty validation fails const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { id: 'did:example:123', - 'field/with~slash': 'test-value' + name: 'Alice' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/field~1with~0slash'] + renderSuite: 'nfc', + // valid template + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', + renderProperty: ['/credentialSubject/nonExistentField'] } }; @@ -566,27 +734,30 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); - should.exist(result.bytes); - const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('test-value'); + should.exist(err); + should.not.exist(result); + err.message.should.contain('Property not found'); } ); - it('should fail when renderProperty is missing', + it('should validate all renderProperty fields exist', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { id: 'did:example:123', - name: 'Alice' + firstName: 'Alice', + lastName: 'Smith' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic' - // No renderProperty + renderSuite: 'nfc', + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', + renderProperty: [ + '/credentialSubject/firstName', + '/credentialSubject/lastName' + ] } }; @@ -598,25 +769,29 @@ describe('NFC Renderer', function() { err = e; } - should.exist(err); - should.not.exist(result); - err.message.should.contain('renderProperty'); + should.not.exist(err); + should.exist(result); + // Template is decoded, not the fields + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('Hello NFC'); } ); - it('should fail when renderProperty path does not exist', + it('should succeed when renderProperty is empty array', async () => { + // Empty renderProperty is treated as "no filtering" const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { - id: 'did:example:123', - name: 'Alice' + id: 'did:example:123' }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/nonExistentField'] + renderSuite: 'nfc', + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', + // Empty is OK + renderProperty: [] } }; @@ -628,24 +803,175 @@ describe('NFC Renderer', function() { err = e; } - should.exist(err); - should.not.exist(result); - err.message.should.contain('Property not found'); + should.not.exist(err); + should.exist(result); + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('Hello NFC'); } ); - it('should fail when renderProperty is empty array', + // TODO: Delete later + // it('should fail when renderProperty is empty array', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123' + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc-dynamic', + // renderProperty: [] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.exist(err); + // should.not.exist(result); + // err.message.should.contain('cannot be empty'); + // } + // ); + }); + + // TODO: Delete later + // describe('renderToNfc() - Generic NFC Suite', function() { + // it('should prioritize static rendering when both payload and ' + + // 'renderProperty exist', async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123', + // name: 'Alice' + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc', + // // "Hello NFC" + // template: 'z2drAj5bAkJFsTPKmBvG3Z', + // renderProperty: ['/credentialSubject/name'] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.not.exist(err); + // should.exist(result); + // // Should use static rendering (template), not dynamic + // const decoded = new TextDecoder().decode(result.bytes); + // // If it was dynamic, it would be "Alice" + // decoded.should.not.equal('Alice'); + // }); + + // it('should fallback to dynamic rendering when' + + // ' only renderProperty exists', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123', + // name: 'Bob' + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc', + // renderProperty: ['/credentialSubject/name'] + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.not.exist(err); + // should.exist(result); + // const decoded = new TextDecoder().decode(result.bytes); + // decoded.should.equal('Bob'); + // } + // ); + + // it('should fail when neither template nor renderProperty exist', + // async () => { + // const credential = { + // '@context': ['https://www.w3.org/ns/credentials/v2'], + // type: ['VerifiableCredential'], + // credentialSubject: { + // id: 'did:example:123' + // }, + // renderMethod: { + // type: 'TemplateRenderMethod', + // renderSuite: 'nfc' + // } + // }; + + // let result; + // let err; + // try { + // result = await webWallet.nfcRenderer.renderToNfc({credential}); + // } catch(e) { + // err = e; + // } + + // should.exist(err); + // should.not.exist(result); + // err.message.should.contain('neither payload nor renderProperty'); + // } + // ); + // }); + + describe('renderToNfc() - Error Cases', function() { + it('should fail when credential has no renderMethod', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], credentialSubject: { id: 'did:example:123' - }, + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('does not support NFC rendering'); + } + ); + + it('should fail when renderSuite is unsupported', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: [] + renderSuite: 'unsupported-suite', + template: 'some-data' } }; @@ -659,59 +985,62 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('cannot be empty'); + err.message.should.contain('does not support NFC rendering'); } ); - }); - describe('renderToNfc() - Generic NFC Suite', function() { - it('should prioritize static rendering when both payload and ' + - 'renderProperty exist', async () => { - const credential = { - '@context': ['https://www.w3.org/ns/credentials/v2'], - type: ['VerifiableCredential'], - credentialSubject: { - id: 'did:example:123', - name: 'Alice' - }, - renderMethod: { - type: 'TemplateRenderMethod', - renderSuite: 'nfc', - // "Hello NFC" - template: 'z2drAj5bAkJFsTPKmBvG3Z', - renderProperty: ['/credentialSubject/name'] + it('should fail when credential parameter is missing', + async () => { + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({}); + } catch(e) { + err = e; } - }; - let result; - let err; - try { - result = await webWallet.nfcRenderer.renderToNfc({credential}); - } catch(e) { - err = e; + should.exist(err); + should.not.exist(result); } + ); - should.not.exist(err); - should.exist(result); - // Should use static rendering (template), not dynamic - const decoded = new TextDecoder().decode(result.bytes); - // If it was dynamic, it would be "Alice" - decoded.should.not.equal('Alice'); - }); + it('should fail when data URI has wrong media type', + async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + // Wrong media type - should be application/octet-stream + template: 'data:text/plain;base64,SGVsbG8=' + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('media type'); + } + ); - it('should fallback to dynamic rendering when only renderProperty exists', + it('should fail when data URI format is malformed', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], - credentialSubject: { - id: 'did:example:123', - name: 'Bob' - }, renderMethod: { type: 'TemplateRenderMethod', renderSuite: 'nfc', - renderProperty: ['/credentialSubject/name'] + // Malformed data URI (missing encoding or data) + template: 'data:application/octet-stream' } }; @@ -723,24 +1052,22 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); - const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('Bob'); + should.exist(err); + should.not.exist(result); + err.message.should.contain('Invalid data URI'); } ); - it('should fail when neither template nor renderProperty exist', + it('should fail when multibase encoding is unsupported', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], - credentialSubject: { - id: 'did:example:123' - }, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc' + renderSuite: 'nfc', + // 'f' is base16 multibase - not supported by implementation + template: 'f48656c6c6f' } }; @@ -754,19 +1081,20 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('neither payload nor renderProperty'); + err.message.should.contain('encoding format'); } ); - }); - describe('renderToNfc() - Error Cases', function() { - it('should fail when credential has no renderMethod', + it('should fail when data URI encoding is unsupported', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], - credentialSubject: { - id: 'did:example:123' + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + // hex encoding is not supported + template: 'data:application/octet-stream;hex,48656c6c6f' } }; @@ -780,19 +1108,20 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('does not support NFC rendering'); + err.message.should.contain('encoding'); } ); - it('should fail when renderSuite is unsupported', + it('should fail when base64 data is invalid', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'unsupported-suite', - template: 'some-data' + renderSuite: 'nfc', + // Invalid base64 characters + template: 'data:application/octet-stream;base64,!!!invalid!!!' } }; @@ -806,16 +1135,30 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('does not support NFC rendering'); + // Error message varies by environment (browser vs Node) } ); - it('should fail when credential parameter is missing', + it('should fail when template is not a string', async () => { + const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + // Template should be a string, not an object + template: { + type: 'embedded', + data: 'some-data' + } + } + }; + let result; let err; try { - result = await webWallet.nfcRenderer.renderToNfc({}); + result = await webWallet.nfcRenderer.renderToNfc({credential}); } catch(e) { err = e; } @@ -824,6 +1167,7 @@ describe('NFC Renderer', function() { should.not.exist(result); } ); + }); describe('NFC Renderer - EAD Credential Tests (from URL)', function() { @@ -889,7 +1233,7 @@ describe('NFC Renderer', function() { } ); - it('should return true when adding nfc-dynamic renderMethod', + it('should return true when adding nfc renderMethod with renderProperty', function() { if(!eadCredential) { this.skip(); @@ -899,8 +1243,9 @@ describe('NFC Renderer', function() { ...eadCredential, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', + renderSuite: 'nfc', renderProperty: ['/credentialSubject/givenName'] + // Note: no template, but supportsNFC() only checks capability } }; @@ -913,7 +1258,7 @@ describe('NFC Renderer', function() { } ); - it('should return true when adding nfc-static renderMethod', + it('should return true when adding nfc renderMethod with template', function() { if(!eadCredential) { this.skip(); @@ -923,8 +1268,8 @@ describe('NFC Renderer', function() { ...eadCredential, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-static', - template: 'z2drAj5bAkJFsTPKmBvG3Z' + renderSuite: 'nfc', + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD' } }; @@ -938,19 +1283,21 @@ describe('NFC Renderer', function() { ); }); - describe('renderToNfc() - EAD Single Field Extraction', function() { - it('should extract givenName from EAD credential', + describe('renderToNfc() - EAD Template Required Tests', function() { + it('should fail when extracting givenName without template', async function() { if(!eadCredential) { this.skip(); } + // In unified architecture, template is required const credential = { ...eadCredential, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', + renderSuite: 'nfc', renderProperty: ['/credentialSubject/givenName'] + // No template - should fail! } }; @@ -962,36 +1309,52 @@ describe('NFC Renderer', function() { err = e; } - should.not.exist(err); - should.exist(result); + should.exist(err); + should.not.exist(result); + err.message.should.contain('template'); - const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('JOHN'); } ); - it('should extract familyName from EAD credential', + it('should succeed when template is provided with renderProperty', async function() { if(!eadCredential) { this.skip(); } + // Encode "JOHN" as base58 multibase for template + // Using TextEncoder + base58 encoding + const johnBytes = new TextEncoder().encode('JOHN'); + const base58 = await import('base58-universal'); + const encodedJohn = 'z' + base58.encode(johnBytes); + const credential = { ...eadCredential, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', - renderProperty: ['/credentialSubject/familyName'] + renderSuite: 'nfc', + template: encodedJohn, + renderProperty: ['/credentialSubject/givenName'] } }; - const result = await webWallet.nfcRenderer.renderToNfc({credential}); + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('SMITH'); + decoded.should.equal('JOHN'); } ); - it('should extract full name (concatenated)', + it('should validate renderProperty fields exist in credential', async function() { if(!eadCredential) { this.skip(); @@ -1001,7 +1364,8 @@ describe('NFC Renderer', function() { ...eadCredential, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', + renderSuite: 'nfc', + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', renderProperty: [ '/credentialSubject/givenName', '/credentialSubject/additionalName', @@ -1010,15 +1374,25 @@ describe('NFC Renderer', function() { } }; - const result = await webWallet.nfcRenderer.renderToNfc({credential}); + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.not.exist(err); + should.exist(result); + // Template is decoded, not the credential fields const decoded = new TextDecoder().decode(result.bytes); - decoded.should.equal('JOHNJACOBSMITH'); + decoded.should.equal('Hello NFC'); } ); }); - describe('renderToNfc() - EAD Image Data', function() { - it('should extract large image data URI', + describe('renderToNfc() - EAD Template Size Tests', function() { + it('should fail when trying to extract image without template', async function() { if(!eadCredential) { this.skip(); @@ -1028,7 +1402,48 @@ describe('NFC Renderer', function() { ...eadCredential, renderMethod: { type: 'TemplateRenderMethod', - renderSuite: 'nfc-dynamic', + renderSuite: 'nfc', + renderProperty: ['/credentialSubject/image'] + // No template - should fail! + } + }; + + let result; + let err; + try { + result = await webWallet.nfcRenderer.renderToNfc({credential}); + } catch(e) { + err = e; + } + + should.exist(err); + should.not.exist(result); + err.message.should.contain('template'); + } + ); + + it('should decode large template successfully', + async function() { + if(!eadCredential) { + this.skip(); + } + + // Get the actual image from credential for comparison + const actualImage = eadCredential.credentialSubject.image; + + // Encode the image as base58 multibase template + const imageBytes = new TextEncoder().encode(actualImage); + const base58 = await import('base58-universal'); + const encodedImage = 'z' + base58.encode(imageBytes); + + const credential = { + ...eadCredential, + renderMethod: { + type: 'TemplateRenderMethod', + renderSuite: 'nfc', + // Large template with image data + template: encodedImage, + // Validates field exists renderProperty: ['/credentialSubject/image'] } }; @@ -1039,16 +1454,14 @@ describe('NFC Renderer', function() { should.exist(result.bytes); const decoded = new TextDecoder().decode(result.bytes); - // Use regex to check starts with decoded.should.match(/^data:image\/png;base64,/); // Verify it's the full large image (should be > 50KB) result.bytes.length.should.be.greaterThan(50000); } ); - }); + }); }); - }); From d566f4c73e76ac2c80805b67bb49cc83d11408c4 Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Wed, 26 Nov 2025 15:19:51 -0800 Subject: [PATCH 6/8] Removes commented code and correct camel case function name. --- lib/helpers.js | 72 ++------------------------------------------------ 1 file changed, 2 insertions(+), 70 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index ba94e3b..5e382ee 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,9 +1,7 @@ /*! * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. */ -// import * as base58 from 'base58-universal'; -// import * as base64url from 'base64url-universal'; -import {renderToNfc, supportsNFC} from './nfcRenderer.js'; +import {renderToNfc, supportsNfc} from './nfcRenderer.js'; import {config} from '@bedrock/web'; import { Ed25519VerificationKey2020 @@ -20,11 +18,6 @@ const supportedSignerTypes = new Map([ ] ]); -// const multibaseDecoders = new Map([ -// ['u', base64url], -// ['z', base58], -// ]); - export async function createCapabilities({profileId, request}) { // TODO: validate `request` @@ -182,71 +175,10 @@ export async function toNFCPayload({credential}) { return renderToNfc({credential}); } -// export async function toNFCPayload({credential}) { -// const nfcRenderingTemplate2024 = -// _getNFCRenderingTemplate2024({credential}); -// const bytes = await _decodeMultibase(nfcRenderingTemplate2024.payload); -// return {bytes}; -// } - export function hasNFCPayload({credential}) { - return supportsNFC({credential}); + return supportsNfc({credential}); } -// export function hasNFCPayload({credential}) { -// try { -// const nfcRenderingTemplate2024 = -// _getNFCRenderingTemplate2024({credential}); -// if(!nfcRenderingTemplate2024) { -// return false; -// } - -// return true; -// } catch(e) { -// return false; -// } -// } - -// function _getNFCRenderingTemplate2024({credential}) { -// let {renderMethod} = credential; -// if(!renderMethod) { -// throw new Error('Credential does not contain "renderMethod".'); -// } - -// renderMethod = Array.isArray(renderMethod) ? renderMethod : [renderMethod]; - -// let nfcRenderingTemplate2024 = null; -// for(const rm of renderMethod) { -// if(rm.type === 'NfcRenderingTemplate2024') { -// nfcRenderingTemplate2024 = rm; -// break; -// } -// continue; -// } - -// if(nfcRenderingTemplate2024 === null) { -// throw new Error( -// 'Credential does not support "NfcRenderingTemplate2024".'); -// } - -// if(!nfcRenderingTemplate2024.payload) { -// throw new Error('NfcRenderingTemplate2024 does not contain "payload".'); -// } - -// return nfcRenderingTemplate2024; -// } - -// async function _decodeMultibase(input) { -// const multibaseHeader = input[0]; -// const decoder = multibaseDecoders.get(multibaseHeader); -// if(!decoder) { -// throw new Error(`Multibase header "${multibaseHeader}" not supported.`); -// } - -// const encodedStr = input.slice(1); -// return decoder.decode(encodedStr); -// } - async function _updateProfileAgentUser({ accessManager, profileAgent, profileAgentContent }) { From 60702cf674e9a3209c2317da0249aed023ad6bff Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Thu, 27 Nov 2025 13:55:42 -0600 Subject: [PATCH 7/8] Pre-filter invalid NFC render methods in detection. Update _findNfcRenderMethod() to only return valid render methods with required template/payload fields. This prevents UI from showing NFC options that won't work. Changes: - Add template/payload validation in _findNfcRenderMethod(). - Remove 'both fields' validation (ignore unknown fields per spec). - Simplify error message for missing template field. - Use TypeError for type validation instead of Error. - Group declaration and validation without whitespace. - Rename supportsNFC to supportsNfc for camelCase consistency. - Rename _findNFCRenderMethod to _findNfcRenderMethod for camelCase consistency. - Update tests to expect 'does not support NFC' error. - Convert 'both fields' error test to success test. --- lib/nfcRenderer.js | 361 +++--------------------------------- test/web/15-nfc-renderer.js | 83 +++++---- 2 files changed, 72 insertions(+), 372 deletions(-) diff --git a/lib/nfcRenderer.js b/lib/nfcRenderer.js index d6aa1ad..3ea1c9e 100644 --- a/lib/nfcRenderer.js +++ b/lib/nfcRenderer.js @@ -26,14 +26,12 @@ const multibaseDecoders = new Map([ * @param {object} options.credential - The verifiable credential. * @returns {boolean} - 'true' if NFC is supported. */ -export function supportsNFC({credential} = {}) { +export function supportsNfc({credential} = {}) { try { - const renderMethod = _findNFCRenderMethod({credential}); - if(renderMethod !== null) { - return true; - } - // no NFC render method found - return false; + const renderMethod = _findNfcRenderMethod({credential}); + + // return whether any NFC render method was found + return renderMethod !== null; } catch(error) { return false; } @@ -60,20 +58,17 @@ export function supportsNFC({credential} = {}) { */ export async function renderToNfc({credential} = {}) { // finc NFC-compatible render method - const renderMethod = _findNFCRenderMethod({credential}); + const renderMethod = _findNfcRenderMethod({credential}); if(!renderMethod) { throw new Error( - 'The verifiable credential does not support NFC rendering.' - ); + 'The verifiable credential does not support NFC rendering.'); } - // require template/payload field + // require template/payload field (safety check - should not reach here + // as _findNfcRenderMethod only returns valid methods) if(!_hasTemplate({renderMethod})) { - throw new Error( - 'NFC rendering requires a template field. ' + - 'The template should contain the pre-encoded NFC payload.' - ); + throw new Error('NFC render method is missing the "template" field.'); } // Step 1: Filter credential if renderProperty exists @@ -89,72 +84,6 @@ export async function renderToNfc({credential} = {}) { return {bytes}; } -// TODO: Delete later. -/** - * Use renderToNFC instead renderToNfc_V0 function. - * Render a verifiable credential to NFC payload bytes. - * - * Supports both static (pre-encoded) and dynamic (runtime extraction) - * rendering modes based on the renderSuite value: - * - "nfc-static": Uses template (W3C spec) or payload (legacy) field. - * - "nfc-dynamic": Extracts data using renderProperty. - * - "nfc": Generic fallback - static takes priority if both exist. - * - * @param {object} options - Options object. - * @param {object} options.credential - The verifiable credential. - * @returns {Promise} Object with bytes property: {bytes: Uint8Array}. - */ -// export async function renderToNfc_V0({credential} = {}) { -// // find NFC-compatible render method -// const renderMethod = _findNFCRenderMethod({credential}); - -// if(!renderMethod) { -// throw new Error( -// 'The verifiable credential does not support NFC rendering.' -// ); -// } - -// // determining rendering mode and route to appropriate handler -// const suite = _getRenderSuite({renderMethod}); - -// if(!suite) { -// throw new Error('Unable to determine render suite for NFC rendering.'); -// } - -// let bytes; - -// switch(suite) { -// case 'nfc-static': -// bytes = await _renderStatic({renderMethod}); -// break; -// case 'nfc-dynamic': -// bytes = await _renderDynamic({renderMethod, credential}); -// break; -// case 'nfc': -// // try static first, fall back to dynamic if renderProperty exists - -// // BEHAVIOR: Static rendering has priority over dynamic rendering. -// // If BOTH template/payload AND renderProperty exist, static is used -// // and renderProperty is ignored (edge case). -// if(_hasStaticPayload({renderMethod})) { -// bytes = await _renderStatic({renderMethod}); -// } else if(renderMethod.renderProperty) { -// // renderProperty exists, proceed with dynamic rendering -// bytes = await _renderDynamic({renderMethod, credential}); -// } else { -// throw new Error( -// 'NFC render method has neither payload nor renderProperty.' -// ); -// } -// break; -// default: -// throw new Error(`Unsupported renderSuite: ${suite}`); -// } - -// // wrap in object for consistent return format -// return {bytes}; -// } - // ======================== // Render method detection // ======================== @@ -175,18 +104,12 @@ function _hasTemplate({renderMethod} = {}) { // enforce field usage based on render method type if(renderMethod.type === 'TemplateRenderMethod') { // W3C Spec format: check for 'template' field - if(renderMethod && renderMethod.template) { - return true; - } - return false; + return renderMethod?.template !== undefined; } if(renderMethod.type === 'NfcRenderingTemplate2024') { // legacy format: check for 'payload' field - if(renderMethod && renderMethod.payload) { - return true; - } - return false; + return renderMethod?.payload !== undefined; } return false; @@ -207,7 +130,7 @@ function _filterCredential({credential, renderMethod} = {}) { const {renderProperty} = renderMethod; // check if renderProperty exists and is not empty - if(!renderProperty || renderProperty.length == 0) { + if(!(renderProperty?.length > 0)) { return null; } @@ -241,9 +164,8 @@ function _filterCredential({credential, renderMethod} = {}) { * @param {object} options.credential - The verifiable credential. * @returns {object|null} The NFC render method or null. */ -function _findNFCRenderMethod({credential} = {}) { +function _findNfcRenderMethod({credential} = {}) { let renderMethods = credential?.renderMethod; - if(!renderMethods) { return null; } @@ -258,76 +180,27 @@ function _findNFCRenderMethod({credential} = {}) { // check for W3C spec format with nfc renderSuite if(method.type === 'TemplateRenderMethod') { const suite = method.renderSuite?.toLowerCase(); - if(suite && suite.startsWith('nfc')) { - return method; + if(suite?.startsWith('nfc')) { + // only return if template field exists (valid render method) + if(method.template !== undefined) { + return method; + } } } // check for legacy format/existing codebase in // bedrock-web-wallet/lib/helper.js file if(method.type === 'NfcRenderingTemplate2024') { - return method; + // only return if payload field exists (valid render method) + if(method.payload !== undefined) { + return method; + } } } return null; } -// TODO: Delete later. -/** - * Get the render suite with fallback for legacy formats. - * - * @private - * @param {object} options - Options object. - * @param {object} options.renderMethod - The render method object. - * @returns {string} The render suite identifier. - */ -// function _getRenderSuite({renderMethod} = {}) { -// // use renderSuite if present -// if(renderMethod.renderSuite) { -// return renderMethod.renderSuite.toLowerCase(); -// } - -// // legacy format defaults to static -// if(renderMethod.type === 'NfcRenderingTemplate2024') { -// return 'nfc-static'; -// } - -// // generic fallback -// return 'nfc'; -// } - -// TODO: Delete later. -/** - * Check if render method has a static payload. - * - * @private - * @param {object} options - Options object. - * @param {object} options.renderMethod - The render method object. - * @returns {boolean} - 'true' if has appropriate field for render method type. - */ -// function _hasStaticPayload({renderMethod} = {}) { -// // enforce field usage based on render method type -// if(renderMethod.type === 'NfcRenderingTemplate2024') { -// // legacy format: check for 'payload' field -// if(renderMethod && renderMethod.payload) { -// return true; -// } -// return false; -// } -// if(renderMethod.type === 'TemplateRenderMethod') { -// // W3C Spec format: check for 'template' field -// if(renderMethod && renderMethod.template) { -// return true; -// } -// return false; -// } -// // if(renderMethod.template || renderMethod.payload) { -// // return true; -// // } -// return false; -// } - // ======================== // NFC rendering engine // ======================== @@ -347,27 +220,12 @@ function _extractTemplate({renderMethod} = {}) { // check W3C spec format first if(renderMethod.type === 'TemplateRenderMethod') { - // validate: should not have both fields - if(renderMethod.template && renderMethod.payload) { - throw new Error( - 'TemplateRenderMethod requires "template". ' + - 'It should not have both fields.' - ); - } - encoded = renderMethod.template; if(!encoded) { throw new Error('TemplateRenderMethod requires "template" field.'); } // check legacy format } else if(renderMethod.type === 'NfcRenderingTemplate2024') { - // validate: should not have both fields - if(renderMethod.template && renderMethod.payload) { - throw new Error( - 'NfcRenderingTemplate2024 should not have both template ' + - 'and payload fields.' - ); - } encoded = renderMethod.payload; if(!encoded) { throw new Error('NfcRenderingTemplate2024 requires "payload" field.'); @@ -401,7 +259,7 @@ async function _decodeTemplateToBytes({renderMethod, filteredData} = {}) { // validate template is a string if(typeof encoded !== 'string') { - throw new Error('Template or payload must be a string.'); + throw new TypeError('Template or payload must be a string.'); } // Rendering: Decode the template to bytes @@ -427,174 +285,6 @@ async function _decodeTemplate({encoded} = {}) { ); } -// ======================== -// Static rendering -// ======================== - -// TODO: Delete later. -/** - * Render static NFC payload. - * - * @param {object} options - Options object. - * @param {object} options.renderMethod - The render method object. - * @returns {Promise} - NFC payload as bytes. - */ -// async function _renderStatic({renderMethod} = {}) { - -// // enforce field usage based on render method type -// let encoded; - -// // get the payload from template or payload field -// // const encoded = renderMethod.template || renderMethod.payload; -// if(renderMethod.type === 'NfcRenderingTemplate2024') { -// if(renderMethod.template && renderMethod.payload) { -// throw new Error('NfcRenderingTemplate2024 should not have' + -// ' both template and payload fields.' -// ); -// } -// // legacy format: ONLY accept 'payload' field -// encoded = renderMethod.payload; -// if(!encoded) { -// throw new Error('NfcRenderingTemplate2024 requires "payload" field.'); -// } -// } else if(renderMethod.type === 'TemplateRenderMethod') { -// // W3C Spec format: ONLY accept 'template' field -// if(renderMethod.template && renderMethod.payload) { -// throw new Error('TemplateRenderMethod requires "template"' + -// ' and should not have both fields.' -// ); -// } -// encoded = renderMethod.template; -// if(!encoded) { -// throw new Error('TemplateRenderMethod requires "template" field.'); -// } -// } else { -// // This should never happen given _findNFCRenderMethod() logic -// throw new Error(`Unsupported render method type: ${renderMethod.type}`); -// } - -// if(typeof encoded !== 'string') { -// throw new Error('Template or payload must be a string.'); -// } - -// // decoded based on format -// if(encoded.startsWith('data:')) { -// // data URI format -// return _decodeDataUri({dataUri: encoded}); -// } -// if(encoded[0] === 'z' || encoded[0] === 'u') { -// // multibase format -// return _decodeMultibase({input: encoded}); -// } -// throw new Error('Unknown payload encoding format'); -// } - -// ======================== -// Dynamic rendering -// ======================== - -// TODO: Delete later -/** - * Render dynamic NFC payload by extracting data from a verifiable - * credential. - * - * @param {object} options - Options object. - * @param {object} options.renderMethod - The render method object. - * @param {object} options.credential - The verifiable credential. - * @returns {Promise} - NFC payload as bytes. - */ -// async function _renderDynamic( -// {renderMethod, credential} = {}) { - -// // validate renderProperty exists -// if(!renderMethod.renderProperty) { -// throw new Error('Dynamic NFC rendering requires renderProperty.'); -// } - -// // normalize to array for consistent handling -// const propertyPaths = Array.isArray(renderMethod.renderProperty) ? -// renderMethod.renderProperty : [renderMethod.renderProperty]; - -// if(propertyPaths.length === 0) { -// throw new Error('renderProperty cannot be empty.'); -// } - -// // extract values from a verifiable credential using JSON pointers -// const extractedValues = []; - -// for(const path of propertyPaths) { -// const value = _resolveJSONPointer({obj: credential, pointer: path}); - -// if(value === undefined) { -// throw new Error(`Property not found in credential: ${path}`); -// } - -// extractedValues.push({path, value}); -// } - -// // build the NFC payload from extracted values -// return _buildDynamicPayload( -// {extractedValues}); -// } - -// TODO: Delete later. -/** - * Build NFC payload from extracted credential values. - * - * @private - * @param {object} options - Options object. - * @param {Array} options.extractedValues - Extracted values with paths. - * @returns {Uint8Array} - NFC payload as bytes. - */ -// function _buildDynamicPayload({extractedValues} = {}) { - -// // simple concatenation of UTF-8 encoded values -// const chunks = []; - -// for(const item of extractedValues) { -// const valueBytes = _encodeValue({value: item.value}); -// chunks.push(valueBytes); -// } - -// // concatenate all chunks into single payload -// const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); -// const result = new Uint8Array(totalLength); - -// let offset = 0; -// for(const chunk of chunks) { -// result.set(chunk, offset); -// offset += chunk.length; -// } - -// return result; -// } - -// TODO: Delete later. -/** - * Encode a value to bytes. - * - * @private - * @param {object} options - Options object. - * @param {*} options.value - The value to encode. - * @returns {Uint8Array} The encoded bytes. - */ -// function _encodeValue({value} = {}) { -// if(typeof value === 'string') { -// // UTF-8 encode strings -// return new TextEncoder().encode(value); -// } -// if(typeof value === 'number') { -// // convert number to string then encode -// return new TextEncoder().encode(String(value)); -// } -// if(typeof value === 'object') { -// // JSON stringify objects -// return new TextEncoder().encode(JSON.stringify(value)); -// } -// // fallback: convert to string -// return new TextEncoder().encode(String(value)); -// } - // ======================== // Decoding utilities // ======================== @@ -612,7 +302,6 @@ async function _decodeTemplate({encoded} = {}) { function _decodeDataUri({dataUri} = {}) { // parse data URI format: data:mime/type;encoding,data const match = dataUri.match(/^data:([^;]+);([^,]+),(.*)$/); - if(!match) { throw new Error('Invalid data URI format.'); } @@ -747,6 +436,6 @@ function _resolveJSONPointer({obj, pointer} = {}) { // ============ export default { - supportsNFC, + supportsNfc, renderToNfc }; diff --git a/test/web/15-nfc-renderer.js b/test/web/15-nfc-renderer.js index 4c43808..3825847 100644 --- a/test/web/15-nfc-renderer.js +++ b/test/web/15-nfc-renderer.js @@ -1,9 +1,7 @@ import * as webWallet from '@bedrock/web-wallet'; -// console.log(webWallet.nfcRenderer); -// console.log(typeof webWallet.nfcRenderer.supportsNFC); describe('NFC Renderer', function() { - describe('supportsNFC()', function() { + describe('supportsNfc()', function() { // Test to verify if a credential supports NFC rendering. it('should return true for credential with nfc renderSuite and template', async () => { @@ -17,17 +15,16 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(true); } ); - it('should return true even when template is missing ' + - '(detection, not validation', + it('should return false when template is missing ' + + '(pre-filters invalid methods)', async () => { - // Note: supportsNFC() only detects NFC capability, it doesn't validate. - // This credential will fail in renderToNfc() due to missing template. + // supportsNfc() should only return true for VALID render methods. const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], @@ -43,9 +40,9 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); - result.should.equal(true); + result.should.equal(false); } ); @@ -61,7 +58,7 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(true); } @@ -79,7 +76,7 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(true); } @@ -103,7 +100,7 @@ describe('NFC Renderer', function() { ] }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(true); } @@ -119,7 +116,7 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(false); } @@ -136,7 +133,7 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(false); } @@ -155,7 +152,7 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({credential}); + const result = webWallet.nfcRenderer.supportsNfc({credential}); should.exist(result); result.should.equal(true); } @@ -252,8 +249,10 @@ describe('NFC Renderer', function() { ); // Field validation: TemplateRenderMethod uses 'template', not 'payload' - it('should fail when TemplateRenderMethod has both template and payload', + it('should ignore payload field when TemplateRenderMethod has both fields', async () => { + // Per spec: unknown fields are ignored + // TemplateRenderMethod uses 'template', 'payload' is ignored const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], @@ -261,7 +260,7 @@ describe('NFC Renderer', function() { type: 'TemplateRenderMethod', renderSuite: 'nfc', // "Hello NFC" - template: 'z2drAj5bAkJFsTPKmBvG3Z', + template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', // "Different" payload: 'uRGlmZmVyZW50' } @@ -275,9 +274,15 @@ describe('NFC Renderer', function() { err = e; } - should.exist(err); - should.not.exist(result); - err.message.should.contain('template'); + // Should succeed - payload is ignored + should.not.exist(err); + should.exist(result); + should.exist(result.bytes); + + // Verify template was used (not payload) + const decoded = new TextDecoder().decode(result.bytes); + decoded.should.equal('Hello NFC'); + decoded.should.not.equal('Different'); } ); @@ -304,7 +309,8 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('payload'); + // Pre-filtered: render method not found (missing template) + err.message.should.contain('does not support NFC'); } ); @@ -317,7 +323,7 @@ describe('NFC Renderer', function() { renderMethod: { type: 'TemplateRenderMethod', renderSuite: 'nfc' - // No template field + // No template field - pre-filtered as invalid } }; @@ -331,7 +337,8 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('template'); + // Pre-filtered: render method not found (missing template) + err.message.should.contain('does not support NFC'); } ); @@ -464,6 +471,7 @@ describe('NFC Renderer', function() { it('should fail when only renderProperty exists without template', async () => { // In unified architecture, template is always required + // Without template, render method is pre-filtered as invalid const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], @@ -488,7 +496,8 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('template'); + // Pre-filtered: render method not found + err.message.should.contain('does not support NFC'); } ); @@ -1196,7 +1205,7 @@ describe('NFC Renderer', function() { } }); - describe('supportsNFC() - EAD from URL', function() { + describe('supportsNfc() - EAD from URL', function() { it('should return false for EAD credential without renderMethod', function() { // Skip if credential wasn't loaded @@ -1209,7 +1218,7 @@ describe('NFC Renderer', function() { const credentialWithoutRenderMethod = {...eadCredential}; delete credentialWithoutRenderMethod.renderMethod; - const result = webWallet.nfcRenderer.supportsNFC({ + const result = webWallet.nfcRenderer.supportsNfc({ credential: credentialWithoutRenderMethod }); @@ -1224,7 +1233,7 @@ describe('NFC Renderer', function() { this.skip(); } - const result = webWallet.nfcRenderer.supportsNFC({ + const result = webWallet.nfcRenderer.supportsNfc({ credential: eadCredential }); @@ -1233,7 +1242,7 @@ describe('NFC Renderer', function() { } ); - it('should return true when adding nfc renderMethod with renderProperty', + it('should return false when adding nfc renderMethod with renderProperty', function() { if(!eadCredential) { this.skip(); @@ -1245,16 +1254,17 @@ describe('NFC Renderer', function() { type: 'TemplateRenderMethod', renderSuite: 'nfc', renderProperty: ['/credentialSubject/givenName'] - // Note: no template, but supportsNFC() only checks capability + // Note: no template, but supportsNfc() only checks capability } }; - const result = webWallet.nfcRenderer.supportsNFC({ + const result = webWallet.nfcRenderer.supportsNfc({ credential: credentialWithDynamic }); should.exist(result); - result.should.equal(true); + // Pre-filtered: returns false (not a valid NFC render method) + result.should.equal(false); } ); @@ -1273,7 +1283,7 @@ describe('NFC Renderer', function() { } }; - const result = webWallet.nfcRenderer.supportsNFC({ + const result = webWallet.nfcRenderer.supportsNfc({ credential }); @@ -1311,8 +1321,8 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('template'); - + // Pre-filtered: render method not found + err.message.should.contain('does not support NFC'); } ); @@ -1418,7 +1428,8 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('template'); + // Pre-filtered: render method not found + err.message.should.contain('does not support NFC'); } ); From ba5780d2816250272195371abe33dbee249574c9 Mon Sep 17 00:00:00 2001 From: Parth Bhatt Date: Mon, 1 Dec 2025 14:28:41 -0600 Subject: [PATCH 8/8] Split base64 utilities to avoid browser polyfills. Separate base64ToBytes implementation into Node.js and browser-specific files to prevent static analyzers from injecting unnecessary Buffer polyfills into browser bundles. Add browser field aliasing to package.json for automatic environment-specific loading. Changes: - Create lib/util.js with Node.js implementation using Buffer. - Create lib/utilBrowser.js with browser implementation using atob. - Add browser field to package.json for file aliasing. - Update nfcRenderer.js to import from util.js (bundler handles switching). - Remove inline _base64ToBytes function from nfcRenderer.js. - Rename dataUri to dataUrl throughout for consistency. - Update error messages to use 'data URL' instead of 'data URI'. - Update test assertions to match new error messages. --- lib/nfcRenderer.js | 50 ++----- lib/util.js | 25 ++++ lib/utilBrowser.js | 46 ++++++ package.json | 3 + test/web/15-nfc-renderer.js | 285 ++---------------------------------- 5 files changed, 98 insertions(+), 311 deletions(-) create mode 100644 lib/util.js create mode 100644 lib/utilBrowser.js diff --git a/lib/nfcRenderer.js b/lib/nfcRenderer.js index 3ea1c9e..f5fdcd5 100644 --- a/lib/nfcRenderer.js +++ b/lib/nfcRenderer.js @@ -9,6 +9,7 @@ */ import * as base58 from 'base58-universal'; import * as base64url from 'base64url-universal'; +import {base64ToBytes} from './util.js'; const multibaseDecoders = new Map([ ['u', base64url], @@ -269,9 +270,9 @@ async function _decodeTemplateToBytes({renderMethod, filteredData} = {}) { } async function _decodeTemplate({encoded} = {}) { - // data URI format + // data URL format if(encoded.startsWith('data:')) { - return _decodeDataUri({dataUri: encoded}); + return _decodeDataUrl({dataUrl: encoded}); } // multibase format (base58 'z' or base64url 'u') @@ -281,7 +282,7 @@ async function _decodeTemplate({encoded} = {}) { throw new Error( 'Unknown template encoding format. ' + - 'Supported formats: data URI (data:...) or multibase (z..., u...)' + 'Supported formats: data URL (data:...) or multibase (z..., u...)' ); } @@ -290,20 +291,20 @@ async function _decodeTemplate({encoded} = {}) { // ======================== /** - * Decode a data URI to bytes. + * Decode a data URL to bytes. * Validates media type is application/octet-stream.. * * @private * @param {object} options - Options object. - * @param {string} options.dataUri - Data URI string. + * @param {string} options.dataUrl - Data URL string. * @returns {Uint8Array} Decoded bytes. - * @throws {Error} If data URI is invalid or has wrong media type. + * @throws {Error} If data URL is invalid or has wrong media type. */ -function _decodeDataUri({dataUri} = {}) { - // parse data URI format: data:mime/type;encoding,data - const match = dataUri.match(/^data:([^;]+);([^,]+),(.*)$/); +function _decodeDataUrl({dataUrl} = {}) { + // parse data URL format: data:mime/type;encoding,data + const match = dataUrl.match(/^data:([^;]+);([^,]+),(.*)$/); if(!match) { - throw new Error('Invalid data URI format.'); + throw new Error('Invalid data URL format.'); } const mimeType = match[1]; @@ -313,7 +314,7 @@ function _decodeDataUri({dataUri} = {}) { // validate media type is application/octet-stream if(mimeType !== 'application/octet-stream') { throw new Error( - 'Invalid data URI media type. ' + + 'Invalid data URL media type. ' + 'NFC templates must use "application/octet-stream" media type. ' + `Found: "${mimeType}"` ); @@ -321,12 +322,12 @@ function _decodeDataUri({dataUri} = {}) { // decode based on encoding if(encoding === 'base64') { - return _base64ToBytes({base64String: data}); + return base64ToBytes({base64String: data}); } if(encoding === 'base64url') { return base64url.decode(data); } - throw new Error(`Unsupported data URI encoding: ${encoding}`); + throw new Error(`Unsupported data URL encoding: ${encoding}`); } /** @@ -349,29 +350,6 @@ function _decodeMultibase({input} = {}) { return decoder.decode(encodedData); } -/** - * Decode standard base64 to bytes. - * - * @private - * @param {object} options - Options object. - * @param {string} options.base64String - Base64 encoded string. - * @returns {Uint8Array} Decoded bytes. - */ -function _base64ToBytes({base64String} = {}) { - // use atob in browser, Buffer in Node - if(typeof atob !== 'undefined') { - const binaryString = atob(base64String); - const bytes = new Uint8Array(binaryString.length); - for(let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - // Node.js environment - return Buffer.from(base64String, 'base64'); -} - // ======================== // JSON pointer utilities // ======================== diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..bdd13ac --- /dev/null +++ b/lib/util.js @@ -0,0 +1,25 @@ +/*! + * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved. + */ + +/** + * Decode base64 string to bytes (Node.js implmentation). + * + * @param {object} options - Options object. + * @param {string} options.base64String - Base64 encoded string. + * @returns {Uint8Array} Decoded bytes. + */ +export function base64ToBytes({base64String} = {}) { + return new Uint8Array(Buffer.from(base64String, 'base64')); +} + +/** + * Encode bytes to base64 string (Node.js implementation). + * + * @param {object} options - Options object. + * @param {Uint8Array} options.bytes - Bytes to encode. + * @returns {string} Base64 encoded string. + */ +export function bytesToBase64({bytes} = {}) { + return Buffer.from(bytes).toString('base64'); +} diff --git a/lib/utilBrowser.js b/lib/utilBrowser.js new file mode 100644 index 0000000..c2012b1 --- /dev/null +++ b/lib/utilBrowser.js @@ -0,0 +1,46 @@ +/*! + * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved. + */ + +/** + * Decode base64 string to bytes (Browser implmentation). + * + * @param {object} options - Options object. + * @param {string} options.base64String - Base64 encoded string. + * @returns {Uint8Array} Decoded bytes. + */ +export function base64ToBytes({base64String} = {}) { + // Use modern API + if(typeof Uint8Array.fromBase64 === 'function') { + return Uint8Array.fromBase64(base64String); + } + + // Fallback to atob + const binaryString = atob(base64String); + const bytes = new Uint8Array(binaryString.length); + for(let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +} + +/** + * Encode bytes to base64 string (Browser implementation). + * + * @param {object} options - Options object. + * @param {Uint8Array} options.bytes - Bytes to encode. + * @returns {string} Base64 encoded string. + */ +export function bytesToBase64({bytes} = {}) { + // Use modern API + if(typeof Uint8Array.prototype.toBase64 === 'function') { + return bytes.toBase64(); + } + + // Fallback to btoa + let binaryString = ''; + for(let i = 0; i < bytes.length; i++) { + binaryString += String.fromCharCode(bytes[i]); + } + return btoa(binaryString); +} diff --git a/package.json b/package.json index a909dac..0198ef8 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "Bedrock Web Wallet", "type": "module", "exports": "./lib/index.js", + "browser": { + "./lib/util.js": "./lib/utilBrowser.js" + }, "files": [ "lib/*" ], diff --git a/test/web/15-nfc-renderer.js b/test/web/15-nfc-renderer.js index 3825847..5118f25 100644 --- a/test/web/15-nfc-renderer.js +++ b/test/web/15-nfc-renderer.js @@ -217,9 +217,9 @@ describe('NFC Renderer', function() { } ); - it('should successfully render static NFC with data URI format', + it('should successfully render static NFC with data URL format', async () => { - // Data URI with base64 encoded "NFC Data" + // Data URL with base64 encoded "NFC Data" const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], @@ -409,7 +409,7 @@ describe('NFC Renderer', function() { renderMethod: { type: 'TemplateRenderMethod', renderSuite: 'nfc', - // "Hello NFC" as base64 in data URI format + // "Hello NFC" as base64 in data URL format template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', // For transparency renderProperty: ['/credentialSubject/greeting'] @@ -519,7 +519,7 @@ describe('NFC Renderer', function() { template: 'data:application/octet-stream;base64,SGVsbG8gTkZD', // Validates field exists renderProperty: [ - '/credentialSubject/name', + '/credentialSubject/name' ] } }; @@ -542,145 +542,6 @@ describe('NFC Renderer', function() { } ); - // TODO: Delete later - // it('should handle numeric values in dynamic rendering', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123', - // age: 25 - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc-dynamic', - // renderProperty: ['/credentialSubject/age'] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.not.exist(err); - // should.exist(result); - // should.exist(result.bytes); - // const decoded = new TextDecoder().decode(result.bytes); - // decoded.should.equal('25'); - - // } - // ); - - // TODO: Delete later - // it('should handle object values in dynamic rendering', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123', - // address: { - // street: '123 Main St', - // city: 'Boston' - // } - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc-dynamic', - // renderProperty: ['/credentialSubject/address'] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.not.exist(err); - // should.exist(result); - // should.exist(result.bytes); - // // Should be JSON stringified - // const decoded = new TextDecoder().decode(result.bytes); - // const parsed = JSON.parse(decoded); - // parsed.street.should.equal('123 Main St'); - // parsed.city.should.equal('Boston'); - // } - // ); - - // TODO: Delete later - // it('should handle array access in JSON pointer', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123', - // skills: ['JavaScript', 'Python', 'Rust'] - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc-dynamic', - // renderProperty: ['/credentialSubject/skills/0'] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.not.exist(err); - // should.exist(result); - // should.exist(result.bytes); - // const decoded = new TextDecoder().decode(result.bytes); - // decoded.should.equal('JavaScript'); - // } - // ); - - // TODO: Delete later - // it('should handle special characters in JSON pointer', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123', - // 'field/with~slash': 'test-value' - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc-dynamic', - // renderProperty: ['/credentialSubject/field~1with~0slash'] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.not.exist(err); - // should.exist(result); - // should.exist(result.bytes); - // const decoded = new TextDecoder().decode(result.bytes); - // decoded.should.equal('test-value'); - // } - // ); - it('should succeed when renderProperty is missing but template exists', async () => { // renderProperty is optional - template is what matters @@ -818,135 +679,8 @@ describe('NFC Renderer', function() { decoded.should.equal('Hello NFC'); } ); - - // TODO: Delete later - // it('should fail when renderProperty is empty array', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123' - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc-dynamic', - // renderProperty: [] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.exist(err); - // should.not.exist(result); - // err.message.should.contain('cannot be empty'); - // } - // ); }); - // TODO: Delete later - // describe('renderToNfc() - Generic NFC Suite', function() { - // it('should prioritize static rendering when both payload and ' + - // 'renderProperty exist', async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123', - // name: 'Alice' - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc', - // // "Hello NFC" - // template: 'z2drAj5bAkJFsTPKmBvG3Z', - // renderProperty: ['/credentialSubject/name'] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.not.exist(err); - // should.exist(result); - // // Should use static rendering (template), not dynamic - // const decoded = new TextDecoder().decode(result.bytes); - // // If it was dynamic, it would be "Alice" - // decoded.should.not.equal('Alice'); - // }); - - // it('should fallback to dynamic rendering when' + - // ' only renderProperty exists', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123', - // name: 'Bob' - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc', - // renderProperty: ['/credentialSubject/name'] - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.not.exist(err); - // should.exist(result); - // const decoded = new TextDecoder().decode(result.bytes); - // decoded.should.equal('Bob'); - // } - // ); - - // it('should fail when neither template nor renderProperty exist', - // async () => { - // const credential = { - // '@context': ['https://www.w3.org/ns/credentials/v2'], - // type: ['VerifiableCredential'], - // credentialSubject: { - // id: 'did:example:123' - // }, - // renderMethod: { - // type: 'TemplateRenderMethod', - // renderSuite: 'nfc' - // } - // }; - - // let result; - // let err; - // try { - // result = await webWallet.nfcRenderer.renderToNfc({credential}); - // } catch(e) { - // err = e; - // } - - // should.exist(err); - // should.not.exist(result); - // err.message.should.contain('neither payload nor renderProperty'); - // } - // ); - // }); - describe('renderToNfc() - Error Cases', function() { it('should fail when credential has no renderMethod', async () => { @@ -1013,7 +747,7 @@ describe('NFC Renderer', function() { } ); - it('should fail when data URI has wrong media type', + it('should fail when data URL has wrong media type', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], @@ -1040,7 +774,7 @@ describe('NFC Renderer', function() { } ); - it('should fail when data URI format is malformed', + it('should fail when data URL format is malformed', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], @@ -1048,7 +782,7 @@ describe('NFC Renderer', function() { renderMethod: { type: 'TemplateRenderMethod', renderSuite: 'nfc', - // Malformed data URI (missing encoding or data) + // Malformed data URL (missing encoding or data) template: 'data:application/octet-stream' } }; @@ -1063,7 +797,7 @@ describe('NFC Renderer', function() { should.exist(err); should.not.exist(result); - err.message.should.contain('Invalid data URI'); + err.message.should.contain('Invalid data URL'); } ); @@ -1094,7 +828,7 @@ describe('NFC Renderer', function() { } ); - it('should fail when data URI encoding is unsupported', + it('should fail when data URL encoding is unsupported', async () => { const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], @@ -1473,6 +1207,7 @@ describe('NFC Renderer', function() { ); }); + }); });