import { v4 as uuidv4 } from 'uuid'; import IframeReference from './IframeReference'; import EventBus from './EventBus'; import User from './User'; import MapUtils from './MapUtils'; import { FileBlob } from '../front/Api/Entities/types'; export default class MessageBus { private static instance: MessageBus; private errors: { [key: string]: string } = {}; private isListening: boolean = false; public static getInstance(): MessageBus { if (!MessageBus.instance) { MessageBus.instance = new MessageBus(); } return MessageBus.instance; } public initMessageListener(): void { window.addEventListener('message', this.handleMessage.bind(this)); } public destroyMessageListener(): void { window.removeEventListener('message', this.handleMessage.bind(this)); } public isReady(): Promise { if (this.isListening) { return Promise.resolve(); } return new Promise((resolve: () => void) => { const unsubscribe = EventBus.getInstance().on('IS_READY', () => { unsubscribe(); resolve(); }); }); } public requestLink(): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { const messageId = `REQUEST_LINK_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('LINK_ACCEPTED', (responseId: string, message: { accessToken: string, refreshToken: string }) => { if (responseId !== messageId) { return; } unsubscribe(); User.getInstance().setTokens(message.accessToken, message.refreshToken); resolve(); }); const unsubscribeError = EventBus.getInstance().on('ERROR_LINK_ACCEPTED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); this.sendMessage({ type: 'REQUEST_LINK', messageId }); }); } public getPairingId(): Promise { return new Promise((resolve: (pairingId: string) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `GET_PAIRING_ID_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('GET_PAIRING_ID', (responseId: string, pairingId: string) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(pairingId); }); const unsubscribeError = EventBus.getInstance().on('ERROR_GET_PAIRING_ID', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'GET_PAIRING_ID', accessToken, messageId }); }).catch(console.error); }); } public getFileByUid(uid: string): Promise { return new Promise((resolve: (files: any[]) => void, reject: (error: string) => void) => { this.getProcesses().then(async (processes: any) => { const files: any[] = []; for (const processId of Object.keys(processes)) { const process = processes[processId]; if (!process.states) { continue; } const publicDataDecoded: { [key: string]: any } = {}; // We only care about the public data as they are in the last commited state const processTip = process.states[process.states.length - 1].commited_in; const lastCommitedState = process.states.findLast((state: any) => state.commited_in !== processTip); if (!lastCommitedState) { continue; } const publicDataEncoded = lastCommitedState.public_data; if (!publicDataEncoded) { continue; } for (const key of Object.keys(publicDataEncoded)) { publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]); } if (!(publicDataDecoded['uid'] && publicDataDecoded['uid'] === uid && publicDataDecoded['utype'] && publicDataDecoded['utype'] === 'file')) { continue; } // We take the file in it's latest commited state let file: any; // Which is the last state that updated file_blob? const lastUpdatedFileState = process.states.findLast((state: any) => state.pcd_commitment['file_blob']); if (!lastUpdatedFileState) { continue; } try { const processData = await this.getData(processId, lastUpdatedFileState.state_id); const isEmpty = Object.keys(processData).length === 0; if (isEmpty) { continue; } const publicDataEncoded = lastUpdatedFileState.public_data; if (!publicDataEncoded) { continue; } if (!file) { file = { processId, lastUpdatedFileState, processData, publicDataDecoded, }; } else { for (const key of Object.keys(processData)) { file.processData[key] = processData[key]; } file.lastUpdatedFileState = lastUpdatedFileState; for (const key of Object.keys(publicDataDecoded)) { file.publicDataDecoded[key] = publicDataDecoded[key]; } } } catch (error) { console.error(error); } files.push(file); break; } resolve(files[0]); }).catch(reject); }); } public getProcessesDecoded(filterPublicValues: (publicValues: { [key: string]: any }) => boolean): Promise { return new Promise((resolve: (processesDecoded: any[]) => void, reject: (error: string) => void) => { this.getProcesses().then(async (processes: any) => { const processesDecoded: any[] = []; for (const processId of Object.keys(processes)) { const process = processes[processId]; if (!process.states) { continue; } const publicDataDecoded: { [key: string]: any } = {}; for (let stateId = 0; stateId < process.states.length - 1; stateId++) { const state = process.states[stateId]; if (!state) { continue; } const publicDataEncoded = state.public_data; if (!publicDataEncoded) { continue; } for (const key of Object.keys(publicDataEncoded)) { publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]); } } if (!filterPublicValues(publicDataDecoded)) { continue; } let processDecoded: any; for (let stateId = 0; stateId < process.states.length - 1; stateId++) { const lastState = process.states[stateId]; if (!lastState) { continue; } const lastStateId = lastState.state_id; if (!lastStateId) { continue; } try { let processData = await this.getData(processId, lastStateId); if (!processData) { continue; } const isEmpty = Object.keys(processData).length === 0; if (isEmpty) { continue; } for (const key of Object.keys(publicDataDecoded)) { processData[key] = publicDataDecoded[key]; } processData = MapUtils.toJson(processData); if (!processDecoded) { processDecoded = { processId, lastStateId, processData, }; } else { for (const key of Object.keys(processData)) { processDecoded.processData[key] = processData[key]; } processDecoded.lastStateId = lastStateId; } } catch (error) { console.error(error); } } processesDecoded.push(processDecoded); } resolve(processesDecoded); }).catch(reject); }); } public createProcess(processData: any, privateFields: string[], roles: {}): Promise { return new Promise((resolve: (processCreated: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `CREATE_PROCESS_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(processCreated); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'CREATE_PROCESS', processData: processData, privateFields: privateFields, roles: roles, accessToken, messageId }); }).catch(console.error); }); } public updateProcess(processId: string, newData: any, privateFields: string[], roles: {} | null): Promise { return new Promise((resolve: (processUpdated: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = uuidv4(); const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, processUpdated: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(processUpdated); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_UPDATED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'UPDATE_PROCESS', processId, newData, privateFields, roles, accessToken, messageId }); }).catch(console.error); }); } public getProcesses(): Promise { return new Promise((resolve: (processes: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `GET_PROCESSES_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => { if (responseId !== messageId) { return; } unsubscribe(); // Filter processes by my processes setTimeout(() => { this.getMyProcesses().then((myProcesses: any) => { const processesFiltered: { [processId: string]: any } = {}; for (const processId of myProcesses) { const process = processes[processId]; if (process) { processesFiltered[processId] = process; } } resolve(processesFiltered); }).catch(reject); }, 500); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESSES_RETRIEVED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'GET_PROCESSES', accessToken, messageId }); }).catch(console.error); }); } public getMyProcesses(): Promise { return new Promise((resolve: (processes: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `GET_MY_PROCESSES_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('GET_MY_PROCESSES', (responseId: string, processes: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(processes); }); const unsubscribeError = EventBus.getInstance().on('ERROR_GET_MY_PROCESSES', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'GET_MY_PROCESSES', accessToken, messageId }); }).catch(console.error); }); } public notifyUpdate(processId: string, stateId: string): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = uuidv4(); const unsubscribe = EventBus.getInstance().on('UPDATE_NOTIFIED', (responseId: string) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(); }); const unsubscribeError = EventBus.getInstance().on('ERROR_UPDATE_NOTIFIED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'NOTIFY_UPDATE', processId, stateId, accessToken, messageId }); }).catch(console.error); }); } public validateState(processId: string, stateId: string): Promise { return new Promise((resolve: (stateValidated: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `VALIDATE_STATE_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, stateValidated: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(stateValidated); }); const unsubscribeError = EventBus.getInstance().on('ERROR_STATE_VALIDATED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'VALIDATE_STATE', processId, stateId, accessToken, messageId }); }).catch(console.error); }); } public getData(processId: string, stateId: string): Promise { return new Promise((resolve: (data: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `DATA_RETRIEVED_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('DATA_RETRIEVED', (responseId: string, data: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(data); }); const unsubscribeError = EventBus.getInstance().on('ERROR_DATA_RETRIEVED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'RETRIEVE_DATA', processId, stateId, accessToken, messageId }); }).catch(console.error); }); } public getPublicData(encodedData: number[]): Promise { return new Promise((resolve: (data: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `PUBLIC_DATA_DECODED_${uuidv4()}` const unsubscribe = EventBus.getInstance().on('PUBLIC_DATA_DECODED', (responseId: string, data: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(data); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PUBLIC_DATA_DECODED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'DECODE_PUBLIC_DATA', encodedData, accessToken, messageId }); }).catch(console.error); }); } /** * Hash a document file using SHA-256 * @param fileBlob - The file blob to hash * @returns Promise - The SHA-256 hash of the file */ public hashDocument(fileBlob: FileBlob, commitedIn: string): Promise { return new Promise((resolve: (hash: string) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `HASH_VALUE_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('VALUE_HASHED', (responseId: string, hash: string) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(hash); }); const unsubscribeError = EventBus.getInstance().on('ERROR_VALUE_HASHED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; const label = 'file_blob'; this.sendMessage({ type: 'HASH_VALUE', accessToken, commitedIn, label, fileBlob, messageId }); }).catch(reject); }); } public generateMerkleProof(processState: any, attributeName: string): Promise { return new Promise((resolve: (proof: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const messageId = `GET_MERKLE_PROOF_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('MERKLE_PROOF_RETRIEVED', (responseId: string, proof: any) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(proof); }); const unsubscribeError = EventBus.getInstance().on('ERROR_MERKLE_PROOF_RETRIEVED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; this.sendMessage({ type: 'GET_MERKLE_PROOF', accessToken, processState, attributeName, messageId }); }).catch(reject); }); } private validateToken(): Promise { return new Promise((resolve: (isValid: boolean) => void, reject: (error: string) => void) => { const messageId = `VALIDATE_TOKEN_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('TOKEN_VALIDATED', (responseId: string, isValid: boolean) => { if (responseId !== messageId) { return; } unsubscribe(); resolve(isValid); }); const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_VALIDATED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const accessToken = user.getAccessToken()!; const refreshToken = user.getRefreshToken()!; this.sendMessage({ type: 'VALIDATE_TOKEN', accessToken, refreshToken, messageId }); }); } private renewToken(): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { const messageId = `RENEW_TOKEN_${uuidv4()}`; const unsubscribe = EventBus.getInstance().on('TOKEN_RENEWED', (responseId: string, message: { accessToken: string, refreshToken: string }) => { if (responseId !== messageId) { return; } unsubscribe(); User.getInstance().setTokens(message.accessToken, message.refreshToken); resolve(); }); const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_RENEWED', (responseId: string, error: string) => { if (responseId !== messageId) { return; } unsubscribeError(); reject(error); }); const user = User.getInstance(); const refreshToken = user.getRefreshToken()!; this.sendMessage({ type: 'RENEW_TOKEN', refreshToken, messageId }); }); } private checkToken(): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { this.isReady().then(() => { this.validateToken().then((isValid: boolean) => { if (!isValid) { this.renewToken().then(resolve).catch(reject); } else { resolve(); } }).catch(reject); }).catch(reject); }); } private sendMessage(message: any): void { const targetOrigin = IframeReference.getTargetOrigin(); if (!targetOrigin) { console.error('[MessageBus] sendMessage: targetOrigin not found'); return; } const iframe = IframeReference.getIframe(); if (!iframe) { console.error('[MessageBus] sendMessage: iframe not found'); return; } // console.log('[MessageBus] sendMessage:', message); iframe.contentWindow?.postMessage(message, targetOrigin); } private handleMessage(event: MessageEvent): void { if (!event.data || event.data.type === 'PassClientScriptReady') { return; } const iframe = IframeReference.getIframe(); if (!iframe) { console.error('[MessageBus] handleMessage: iframe not found'); return; } if (event.source !== iframe.contentWindow) { console.error('[MessageBus] handleMessage: source not match'); return; } const targetOrigin = IframeReference.getTargetOrigin(); if (!targetOrigin) { console.error('[MessageBus] handleMessage: targetOrigin not found'); return; } if (event.origin !== targetOrigin) { console.error('[MessageBus] handleMessage: origin not match'); return; } if (!event.data || typeof event.data !== 'object') { console.error('[MessageBus] handleMessage: data not found'); return; } const message = event.data; // console.log('[MessageBus] handleMessage:', message); switch (message.type) { case 'LISTENING': this.isListening = true; EventBus.getInstance().emit('IS_READY'); break; case 'LINK_ACCEPTED': this.doHandleMessage(message.messageId, 'LINK_ACCEPTED', message, (message: any) => ({ accessToken: message.accessToken, refreshToken: message.refreshToken })); break; case 'VALIDATE_TOKEN': this.doHandleMessage(message.messageId, 'TOKEN_VALIDATED', message, (message: any) => message.isValid); break; case 'RENEW_TOKEN': this.doHandleMessage(message.messageId, 'TOKEN_RENEWED', message, (message: any) => ({ accessToken: message.accessToken, refreshToken: message.refreshToken })); break; case 'GET_PAIRING_ID': this.doHandleMessage(message.messageId, 'GET_PAIRING_ID', message, (message: any) => message.userPairingId); break; case 'PROCESS_CREATED': // CREATE_PROCESS this.doHandleMessage(message.messageId, 'PROCESS_CREATED', message, (message: any) => message.processCreated); break; case 'PROCESS_UPDATED': // UPDATE_PROCESS this.doHandleMessage(message.messageId, 'PROCESS_UPDATED', message, (message: any) => message.updatedProcess); break; case 'PROCESSES_RETRIEVED': // GET_PROCESSES this.doHandleMessage(message.messageId, 'PROCESSES_RETRIEVED', message, (message: any) => message.processes); break; case 'GET_MY_PROCESSES': // GET_MY_PROCESSES this.doHandleMessage(message.messageId, 'GET_MY_PROCESSES', message, (message: any) => message.myProcesses); break; case 'DATA_RETRIEVED': // RETRIEVE_DATA this.doHandleMessage(message.messageId, 'DATA_RETRIEVED', message, (message: any) => message.data); break; case 'PUBLIC_DATA_DECODED': // DECODE_PUBLIC_DATA this.doHandleMessage(message.messageId, 'PUBLIC_DATA_DECODED', message, (message: any) => message.decodedData); break; case 'UPDATE_NOTIFIED': // NOTIFY_UPDATE this.doHandleMessage(message.messageId, 'UPDATE_NOTIFIED', message, () => { }); break; case 'STATE_VALIDATED': // VALIDATE_STATE this.doHandleMessage(message.messageId, 'STATE_VALIDATED', message, (message: any) => message.validatedProcess); break; case 'VALUE_HASHED': // HASH_VALUE this.doHandleMessage(message.messageId, 'VALUE_HASHED', message, (message: any) => message.hash); break; case 'MERKLE_PROOF_RETRIEVED': // GENERATE_MERKLE_PROOF this.doHandleMessage(message.messageId, 'MERKLE_PROOF_RETRIEVED', message, (message: any) => message.proof); break; case 'ERROR': console.error('Error:', message); this.errors[message.messageId] = message.error; break; } } private doHandleMessage(messageId: string, messageType: string, message: any, callback: (message: any) => any) { if (this.errors[messageId]) { const error = this.errors[messageId]; delete this.errors[messageId]; EventBus.getInstance().emit(`ERROR_${messageType}`, messageId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit(messageType, messageId, callback(message)); } }