helper.js (8277B)
1 // @ts-check 2 // Import the types from the TypeScript file 3 /** 4 * @typedef {import('../dc-types').GetProtocol} GetProtocol 5 * @typedef {import('../dc-types').DigitalCredentialGetRequest} DigitalCredentialGetRequest 6 * @typedef {import('../dc-types').CredentialRequestOptions} CredentialRequestOptions 7 * @typedef {import('../dc-types').CreateProtocol} CreateProtocol 8 * @typedef {import('../dc-types').DigitalCredentialCreateRequest} DigitalCredentialCreateRequest 9 * @typedef {import('../dc-types').CredentialCreationOptions} CredentialCreationOptions 10 * @typedef {import('../dc-types').SendMessageData} SendMessageData 11 * @typedef {import('../dc-types').MakeGetOptionsConfig} MakeGetOptionsConfig 12 * @typedef {import('../dc-types').MakeCreateOptionsConfig} MakeCreateOptionsConfig 13 * @typedef {import('../dc-types').CredentialMediationRequirement} CredentialMediationRequirement 14 * @typedef {import('../dc-types').MobileDocumentRequest} MobileDocumentRequest 15 * @typedef {GetProtocol | CreateProtocol} Protocol 16 */ 17 18 /** @type {Record<Protocol, object | MobileDocumentRequest>} */ 19 const CANONICAL_REQUEST_OBJECTS = { 20 openid4vci: { 21 /* Canonical object coming soon */ 22 }, 23 "openid4vp-v1-unsigned": { 24 /* Canonical object coming soon */ 25 }, 26 "openid4vp-v1-signed": { 27 /* Canonical object coming soon */ 28 }, 29 "openid4vp-v1-multisigned": { 30 /* Canonical object coming soon */ 31 }, 32 /** @type MobileDocumentRequest **/ 33 "org-iso-mdoc": { 34 deviceRequest: 35 "omd2ZXJzaW9uYzEuMGtkb2NSZXF1ZXN0c4GhbGl0ZW1zUmVxdWVzdNgYWIKiZ2RvY1R5cGV1b3JnLmlzby4xODAxMy41LjEubURMam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4x9pWthZ2Vfb3Zlcl8yMfRqZ2l2ZW5fbmFtZfRrZmFtaWx5X25hbWX0cmRyaXZpbmdfcHJpdmlsZWdlc_RocG9ydHJhaXT0", 36 encryptionInfo: 37 "gmVkY2FwaaJlbm9uY2VYICBetSsDkKlE_G9JSIHwPzr3ctt6Ol9GgmCH8iGdGQNJcnJlY2lwaWVudFB1YmxpY0tleaQBAiABIVggKKm1iPeuOb9bDJeeJEL4QldYlWvY7F_K8eZkmYdS9PwiWCCm9PLEmosiE_ildsE11lqq4kDkjhfQUKPpbX-Hm1ZSLg", 38 }, 39 }; 40 41 /** 42 * Internal helper to create final options from a list of requests. 43 * 44 * @template {DigitalCredentialGetRequest[] | DigitalCredentialCreateRequest[]} TRequests 45 * @template {CredentialRequestOptions | CredentialCreationOptions} TOptions 46 * @param {TRequests} requests 47 * @param {CredentialMediationRequirement} [mediation] 48 * @param {AbortSignal} [signal] 49 * @returns {TOptions} 50 */ 51 function makeOptionsFromRequests(requests, mediation, signal) { 52 /** @type {TOptions} */ 53 const options = /** @type {TOptions} */ ({ digital: { requests } }); 54 55 if (mediation) { 56 options.mediation = mediation; 57 } 58 59 if (signal) { 60 options.signal = signal; 61 } 62 63 return options; 64 } 65 66 /** 67 * Build requests from protocols, using canonical data for each protocol. 68 * For create operations with explicit data, uses that data for all protocols. 69 * 70 * @template Req 71 * @param {Protocol[]} protocols 72 * @param {Record<string, (data?: MobileDocumentRequest | object) => Req>} mapping 73 * @param {MobileDocumentRequest | object} [explicitData] - Explicit data for create operations 74 * @returns {Req[]} 75 * @throws {Error} If an unknown protocol string is encountered. 76 */ 77 function buildRequestsFromProtocols(protocols, mapping, explicitData) { 78 return protocols.map((protocol) => { 79 if (!(protocol in mapping)) { 80 throw new Error(`Unknown request type within array: ${protocol}`); 81 } 82 // Use explicit data if provided (for create with data), otherwise canonical data 83 return mapping[protocol](explicitData); 84 }); 85 } 86 87 /** @type {{ 88 * get: Record<GetProtocol, (data?: MobileDocumentRequest | object) => DigitalCredentialGetRequest>; 89 * create: Record<CreateProtocol, (data?: object) => DigitalCredentialCreateRequest>; 90 * }} */ 91 const allMappings = { 92 get: { 93 "org-iso-mdoc": ( 94 data = { ...CANONICAL_REQUEST_OBJECTS["org-iso-mdoc"] }, 95 ) => { 96 return { protocol: "org-iso-mdoc", data }; 97 }, 98 "openid4vp-v1-unsigned": ( 99 data = { ...CANONICAL_REQUEST_OBJECTS["openid4vp-v1-unsigned"] }, 100 ) => { 101 return { protocol: "openid4vp-v1-unsigned", data }; 102 }, 103 "openid4vp-v1-signed": ( 104 data = { ...CANONICAL_REQUEST_OBJECTS["openid4vp-v1-signed"] }, 105 ) => { 106 return { protocol: "openid4vp-v1-signed", data }; 107 }, 108 "openid4vp-v1-multisigned": ( 109 data = { ...CANONICAL_REQUEST_OBJECTS["openid4vp-v1-multisigned"] }, 110 ) => { 111 return { protocol: "openid4vp-v1-multisigned", data }; 112 }, 113 }, 114 create: { 115 "openid4vci": (data = { ...CANONICAL_REQUEST_OBJECTS["openid4vci"] }) => { 116 return { protocol: "openid4vci", data }; 117 }, 118 }, 119 }; 120 121 /** 122 * Generic helper to create credential options from config with protocol already set. 123 * @template {MakeGetOptionsConfig | MakeCreateOptionsConfig} TConfig 124 * @template {DigitalCredentialGetRequest | DigitalCredentialCreateRequest} TRequest 125 * @template {CredentialRequestOptions | CredentialCreationOptions} TOptions 126 * @param {TConfig} config - Configuration options with protocol already defaulted 127 * @param {Record<string, (data?: MobileDocumentRequest | object) => TRequest>} mapping - Protocol to request mapping 128 * @returns {TOptions} 129 */ 130 function makeCredentialOptionsFromConfig(config, mapping) { 131 const { protocol, requests = [], data, mediation, signal } = config; 132 133 // Validate that we have either a protocol or requests 134 if (!protocol && !requests?.length) { 135 throw new Error("No protocol. Can't make options."); 136 } 137 138 /** @type {TRequest[]} */ 139 const allRequests = []; 140 141 allRequests.push(.../** @type {TRequest[]} */ (requests)); 142 143 if (protocol) { 144 const protocolArray = Array.isArray(protocol) ? protocol : [protocol]; 145 const protocolRequests = buildRequestsFromProtocols(protocolArray, mapping, data); 146 allRequests.push(...protocolRequests); 147 } 148 149 return /** @type {TOptions} */ (makeOptionsFromRequests(allRequests, mediation, signal)); 150 } 151 152 /** 153 * Creates options for getting credentials. 154 * @export 155 * @param {MakeGetOptionsConfig} [config={}] - Configuration options 156 * @returns {CredentialRequestOptions} 157 */ 158 export function makeGetOptions(config = {}) { 159 /** @type {MakeGetOptionsConfig} */ 160 const configWithDefaults = { 161 protocol: ["openid4vp-v1-unsigned", "org-iso-mdoc"], 162 ...config, 163 }; 164 165 return /** @type {CredentialRequestOptions} */ ( 166 makeCredentialOptionsFromConfig(configWithDefaults, allMappings.get) 167 ); 168 } 169 170 /** 171 * Creates options for creating credentials. 172 * @export 173 * @param {MakeCreateOptionsConfig} [config={}] - Configuration options 174 * @returns {CredentialCreationOptions} 175 */ 176 export function makeCreateOptions(config = {}) { 177 /** @type {MakeCreateOptionsConfig} */ 178 const configWithDefaults = { 179 protocol: "openid4vci", 180 ...config, 181 }; 182 183 return /** @type {CredentialCreationOptions} */ ( 184 makeCredentialOptionsFromConfig(configWithDefaults, allMappings.create) 185 ); 186 } 187 188 /** 189 * Sends a message to an iframe and return the response. 190 * 191 * @param {HTMLIFrameElement} iframe - The iframe element to send the message to. 192 * @param {SendMessageData} data - The data to be sent to the iframe. 193 * @returns {Promise<any>} - A promise that resolves with the response from the iframe. 194 */ 195 export function sendMessage(iframe, data) { 196 return new Promise((resolve, reject) => { 197 if (!iframe.contentWindow) { 198 reject( 199 new Error( 200 "iframe.contentWindow is undefined, cannot send message (something is wrong with the test that called this).", 201 ), 202 ); 203 return; 204 } 205 window.addEventListener("message", function messageListener(event) { 206 if (event.source === iframe.contentWindow) { 207 window.removeEventListener("message", messageListener); 208 resolve(event.data); 209 } 210 }); 211 iframe.contentWindow.postMessage(data, "*"); 212 }); 213 } 214 215 /** 216 * Load an iframe with the specified URL and wait for it to load. 217 * 218 * @param {HTMLIFrameElement} iframe 219 * @param {string|URL} url 220 * @returns {Promise<void>} 221 */ 222 export function loadIframe(iframe, url) { 223 return new Promise((resolve, reject) => { 224 iframe.addEventListener("load", () => resolve(), { once: true }); 225 iframe.addEventListener("error", (event) => reject(event.error), { 226 once: true, 227 }); 228 if (!iframe.isConnected) { 229 document.body.appendChild(iframe); 230 } 231 iframe.src = url.toString(); 232 }); 233 }