lecoffre-front/src/sdk/MessageBus.ts
2025-07-03 18:09:01 +02:00

861 lines
27 KiB
TypeScript

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<void> {
if (this.isListening) {
return Promise.resolve();
}
return new Promise<void>((resolve: () => void) => {
const unsubscribe = EventBus.getInstance().on('IS_READY', () => {
unsubscribe();
resolve();
});
});
}
public requestLink(): Promise<void> {
return new Promise<void>((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<string> {
return new Promise<string>((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<any> {
return new Promise<any>((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<any[]> {
return new Promise<any[]>((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<any> {
return new Promise<any>((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<any> {
return new Promise<any>((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<any> {
return new Promise<any>((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<any> {
return new Promise<any>((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<void> {
return new Promise<void>((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<any> {
return new Promise<any>((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<any> {
return new Promise<any>((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<any> {
return new Promise<any>((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<string> - The SHA-256 hash of the file
*/
public hashDocument(fileBlob: FileBlob, commitedIn: string): Promise<string> {
return new Promise<string>((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<any> {
return new Promise<any>((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<boolean> {
return new Promise<boolean>((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<void> {
return new Promise<void>((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<void> {
return new Promise<void>((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));
}
}