import '../public/style/4nk.css'; import { initHeader } from '../src/components/header/header'; /*import { initChat } from '../src/pages/chat/chat';*/ import Database from './services/database.service'; import Services from './services/service'; import TokenService from './services/token'; import { cleanSubscriptions } from './utils/subscription.utils'; import { LoginComponent } from './pages/home/home-component'; import { prepareAndSendPairingTx } from './utils/sp-address.utils'; import ModalService from './services/modal.service'; import { MessageType } from './models/process.model'; import { splitPrivateData, isValid32ByteHex } from './utils/service.utils'; import { MerkleProofResult } from 'pkg/sdk_client'; import { isValid } from './models/backup.model'; const routes: { [key: string]: string } = { home: '/src/pages/home/home.html', process: '/src/pages/process/process.html', 'process-element': '/src/pages/process-element/process-element.html', account: '/src/pages/account/account.html', chat: '/src/pages/chat/chat.html', signature: '/src/pages/signature/signature.html', }; export let currentRoute = ''; export async function navigate(path: string) { cleanSubscriptions(); cleanPage(); path = path.replace(/^\//, ''); if (path.includes('/')) { const parsedPath = path.split('/')[0]; if (!routes[parsedPath]) { path = 'home'; } } await handleLocation(path); } async function handleLocation(path: string) { const parsedPath = path.split('/'); if (path.includes('/')) { path = parsedPath[0]; } currentRoute = path; const routeHtml = routes[path] || routes['home']; const content = document.getElementById('containerId'); if (content) { if (path === 'home') { const login = LoginComponent; const container = document.querySelector('#containerId'); const accountComponent = document.createElement('login-4nk-component'); accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;'); if (container) container.appendChild(accountComponent); } else if (path !== 'process') { const html = await fetch(routeHtml).then((data) => data.text()); content.innerHTML = html; } await new Promise(requestAnimationFrame); injectHeader(); // const modalService = await ModalService.getInstance() // modalService.injectValidationModal() switch (path) { case 'process': // const { init } = await import('./pages/process/process'); //const { ProcessListComponent } = await import('./pages/process/process-list-component'); const container2 = document.querySelector('#containerId'); const accountComponent = document.createElement('process-list-4nk-component'); //if (!customElements.get('process-list-4nk-component')) { //customElements.define('process-list-4nk-component', ProcessListComponent); //} accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;'); if (container2) container2.appendChild(accountComponent); break; case 'process-element': if (parsedPath && parsedPath.length) { const { initProcessElement } = await import('./pages/process-element/process-element'); const parseProcess = parsedPath[1].split('_'); initProcessElement(parseProcess[0], parseProcess[1]); } break; case 'account': const { AccountComponent } = await import('./pages/account/account-component'); const accountContainer = document.querySelector('.parameter-list'); if (accountContainer) { if (!customElements.get('account-component')) { customElements.define('account-component', AccountComponent); } const accountComponent = document.createElement('account-component'); accountContainer.appendChild(accountComponent); } break; /*case 'chat': const { ChatComponent } = await import('./pages/chat/chat-component'); const chatContainer = document.querySelector('.group-list'); if (chatContainer) { if (!customElements.get('chat-component')) { customElements.define('chat-component', ChatComponent); } const chatComponent = document.createElement('chat-component'); chatContainer.appendChild(chatComponent); } break;*/ case 'signature': const { SignatureComponent } = await import('./pages/signature/signature-component'); const container = document.querySelector('.group-list'); if (container) { if (!customElements.get('signature-component')) { customElements.define('signature-component', SignatureComponent); } const signatureComponent = document.createElement('signature-component'); container.appendChild(signatureComponent); } break; } } } window.onpopstate = async () => { const services = await Services.getInstance(); if (!services.isPaired()) { handleLocation('home'); } else { handleLocation('process'); } }; export async function init(): Promise { try { const services = await Services.getInstance(); (window as any).myService = services; const db = await Database.getInstance(); db.registerServiceWorker('/src/service-workers/database.worker.js'); const device = await services.getDeviceFromDatabase(); console.log('🚀 ~ setTimeout ~ device:', device); if (!device) { await services.createNewDevice(); } else { services.restoreDevice(device); } // If we create a new device, we most probably don't have anything in db, but just in case await services.restoreProcessesFromDB(); await services.restoreSecretsFromDB(); // We connect to all relays now await services.connectAllRelays(); // We register all the event listeners if we run in an iframe if (window.self !== window.top) { await registerAllListeners(); } if (services.isPaired()) { await navigate('account'); } else { await navigate('home'); } } catch (error) { console.error(error); await navigate('home'); } } export async function registerAllListeners() { const services = await Services.getInstance(); const tokenService = await TokenService.getInstance(); const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { window.parent.postMessage( { type: MessageType.ERROR, error: errorMsg, messageId }, origin ); } // --- Handler functions --- const handleRequestLink = async (event: MessageEvent) => { if (event.data.type !== MessageType.REQUEST_LINK) { return; } const modalService = await ModalService.getInstance(); const result = await modalService.showConfirmationModal({ title: 'Confirmation de liaison', content: ` `, confirmText: 'Ajouter un service', cancelText: 'Annuler' }, true); try { if (!result) { throw new Error('User refused to link'); } if (!services.isPaired()) { // New device - do pairing process console.log('🚀 The device is not paired'); await prepareAndSendPairingTx(); await services.confirmPairing(); } } catch (error) { const errorMsg = `Failed to pair device: ${error}`; errorResponse(errorMsg, event.origin, event.data.messageId); } try { const tokens = await tokenService.generateSessionToken(event.origin); const acceptedMsg = { type: MessageType.LINK_ACCEPTED, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, messageId: event.data.messageId }; window.parent.postMessage( acceptedMsg, event.origin ); } catch (error) { const errorMsg = `Failed to generate tokens: ${error}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleGetMyProcesses = async (event: MessageEvent) => { if (event.data.type !== MessageType.GET_MY_PROCESSES) { return; } if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const myProcesses = await services.getMyProcesses(); window.parent.postMessage( { type: MessageType.GET_MY_PROCESSES, myProcesses, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to get processes: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleGetProcesses = async (event: MessageEvent) => { if (event.data.type !== MessageType.GET_PROCESSES) { return; } const tokenService = await TokenService.getInstance(); if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { accessToken } = event.data; // Validate the session token if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const processes = await services.getProcesses(); window.parent.postMessage( { type: MessageType.PROCESSES_RETRIEVED, processes, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to get processes: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } /// We got a state for some process and return as many clear attributes as we can const handleDecryptState = async (event: MessageEvent) => { if (event.data.type !== MessageType.RETRIEVE_DATA) { return; } const tokenService = await TokenService.getInstance(); if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { processId, stateId, accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } // Retrieve the state for the process const process = await services.getProcess(processId); if (!process) { throw new Error('Can\'t find process'); } const state = services.getStateFromId(process, stateId); let res: Record = {}; if (state) { // Decrypt all the data we have the key for for (const attribute of Object.keys(state.pcd_commitment)) { if (attribute === 'roles' || state.public_data[attribute]) { continue; } const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); if (decryptedAttribute) { res[attribute] = decryptedAttribute; } } } else { throw new Error('Unknown state for process', processId); } window.parent.postMessage( { type: MessageType.DATA_RETRIEVED, data: res, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to retrieve data: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleValidateToken = async (event: MessageEvent) => { if (event.data.type !== MessageType.VALIDATE_TOKEN) { return; } const accessToken = event.data.accessToken; const refreshToken = event.data.refreshToken; if (!accessToken || !refreshToken) { errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId); } const isValid = await tokenService.validateToken(accessToken, event.origin); window.parent.postMessage( { type: MessageType.VALIDATE_TOKEN, accessToken: accessToken, refreshToken: refreshToken, isValid: isValid, messageId: event.data.messageId }, event.origin ); }; const handleRenewToken = async (event: MessageEvent) => { if (event.data.type !== MessageType.RENEW_TOKEN) { return; } try { const refreshToken = event.data.refreshToken; if (!refreshToken) { throw new Error('No refresh token provided'); } const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); if (!newAccessToken) { throw new Error('Failed to refresh token'); } window.parent.postMessage( { type: MessageType.RENEW_TOKEN, accessToken: newAccessToken, refreshToken: refreshToken, messageId: event.data.messageId }, event.origin ); } catch (error) { const errorMsg = `Failed to renew token: ${error}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleGetPairingId = async (event: MessageEvent) => { if (event.data.type !== MessageType.GET_PAIRING_ID) return; if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const userPairingId = services.getPairingProcessId(); window.parent.postMessage( { type: MessageType.GET_PAIRING_ID, userPairingId, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to get pairing id: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleCreateProcess = async (event: MessageEvent) => { if (event.data.type !== MessageType.CREATE_PROCESS) return; if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { processData, privateFields, roles, accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const { privateData, publicData } = splitPrivateData(processData, privateFields); const createProcessReturn = await services.createProcess(privateData, publicData, roles); if (!createProcessReturn.updated_process) { throw new Error('Empty updated_process in createProcessReturn'); } const processId = createProcessReturn.updated_process.process_id; const process = createProcessReturn.updated_process.current_process; const stateId = process.states[0].state_id; await services.handleApiReturn(createProcessReturn); const res = { processId, process, processData, } window.parent.postMessage( { type: MessageType.PROCESS_CREATED, processCreated: res, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to create process: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleNotifyUpdate = async (event: MessageEvent) => { if (event.data.type !== MessageType.NOTIFY_UPDATE) return; if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { processId, stateId, accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } if (!isValid32ByteHex(stateId)) { throw new Error('Invalid state id'); } const res = await services.createPrdUpdate(processId, stateId); await services.handleApiReturn(res); window.parent.postMessage( { type: MessageType.UPDATE_NOTIFIED, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to notify update for process: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleValidateState = async (event: MessageEvent) => { if (event.data.type !== MessageType.VALIDATE_STATE) return; if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { processId, stateId, accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const res = await services.approveChange(processId, stateId); await services.handleApiReturn(res); window.parent.postMessage( { type: MessageType.STATE_VALIDATED, validatedProcess: res.updated_process, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to validate process: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleUpdateProcess = async (event: MessageEvent) => { if (event.data.type !== MessageType.UPDATE_PROCESS) return; if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); } try { // privateFields is only used if newData contains new fields // roles can be empty meaning that roles from the last commited state are kept const { processId, newData, privateFields, roles, accessToken } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } // Check if the new data is already in the process or if it's a new field const process = await services.getProcess(processId); if (!process) { throw new Error('Process not found'); } let lastState = services.getLastCommitedState(process); if (!lastState) { const firstState = process.states[0]; const roles = firstState.roles; if (services.rolesContainsUs(roles)) { const approveChangeRes= await services.approveChange(processId, firstState.state_id); await services.handleApiReturn(approveChangeRes); const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id); await services.handleApiReturn(prdUpdateRes); } else { if (firstState.validation_tokens.length > 0) { // Try to send it again anyway const res = await services.createPrdUpdate(processId, firstState.state_id); await services.handleApiReturn(res); } } // Wait a couple seconds await new Promise(resolve => setTimeout(resolve, 2000)); lastState = services.getLastCommitedState(process); if (!lastState) { throw new Error('Process doesn\'t have a commited state yet'); } } const lastStateIndex = services.getLastCommitedStateIndex(process); if (lastStateIndex === null) { throw new Error('Process doesn\'t have a commited state yet'); } // Shouldn't happen const privateData: Record = {}; const publicData: Record = {}; for (const field of Object.keys(newData)) { // Public data are carried along each new state // So the first thing we can do is check if the new data is public data if (lastState.public_data[field]) { // Add it to public data publicData[field] = newData[field]; continue; } // If it's not a public data, it may be either a private data update, or a new field (public of private) // Caller gave us a list of new private fields, if we see it here this is a new private field if (privateFields.includes(field)) { // Add it to private data privateData[field] = newData[field]; continue; } // Now it can be an update of private data or a new public data // We check that the field exists in previous states private data for (let i = lastStateIndex; i >= 0; i--) { const state = process.states[i]; if (state.pcd_commitment[field]) { // We don't even check if it's a public field, we would have seen it in the last state privateData[field] = newData[field]; break; } else { // This attribute was not modified in that state, we go back to the previous state continue; } } if (privateData[field]) continue; // We've get back all the way to the first state without seeing it, it's a new public field publicData[field] = newData[field]; } // We'll let the wasm check if roles are consistent const res = await services.updateProcess(process, privateData, publicData, roles); await services.handleApiReturn(res); window.parent.postMessage( { type: MessageType.PROCESS_UPDATED, updatedProcess: res.updated_process, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to update process: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleDecodePublicData = async (event: MessageEvent) => { if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) return; if (!services.isPaired()) { const errorMsg = 'Device not paired'; errorResponse(errorMsg, event.origin, event.data.messageId); return; } try { const { accessToken, encodedData } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const decodedData = services.decodeValue(encodedData); window.parent.postMessage( { type: MessageType.PUBLIC_DATA_DECODED, decodedData, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to decode data: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleHashValue = async (event: MessageEvent) => { if (event.data.type !== MessageType.HASH_VALUE) return; console.log('handleHashValue', event.data); try { const { accessToken, commitedIn, label, fileBlob } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const hash = services.getHashForFile(commitedIn, label, fileBlob); window.parent.postMessage( { type: MessageType.VALUE_HASHED, hash, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to hash value: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleGetMerkleProof = async (event: MessageEvent) => { if (event.data.type !== MessageType.GET_MERKLE_PROOF) return; try { const { accessToken, processState, attributeName } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const proof = services.getMerkleProofForFile(processState, attributeName); window.parent.postMessage( { type: MessageType.MERKLE_PROOF_RETRIEVED, proof, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to get merkle proof: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleValidateMerkleProof = async (event: MessageEvent) => { if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return; try { const { accessToken, merkleProof, documentHash } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } // Try to parse the proof // We will validate it's a MerkleProofResult in the wasm let parsedMerkleProof: MerkleProofResult; try { parsedMerkleProof= JSON.parse(merkleProof); } catch (e) { throw new Error('Provided merkleProof is not a valid json object'); } const res = services.validateMerkleProof(parsedMerkleProof, documentHash); window.parent.postMessage( { type: MessageType.MERKLE_PROOF_VALIDATED, isValid: res, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to get merkle proof: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleExportBackup = async (event: MessageEvent) => { if (event.data.type !== MessageType.EXPORT_BACKUP) return; console.log('handleExportBackup', event.data); try { const { accessToken, password } = event.data; if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { throw new Error('Invalid or expired session token'); } const backup = await services.exportUserDataBackup(password); window.parent.postMessage( { type: MessageType.BACKUP_RETRIEVED, backupFile: JSON.stringify(backup), messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to export backup: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } const handleImportBackup = async (event: MessageEvent) => { if (event.data.type !== MessageType.IMPORT_BACKUP) return; console.log('handleImportBackup', event.data); try { const { backupFile } = event.data; // We don't validate a token here const backup = JSON.parse(backupFile); if (!isValid(backup)) { throw new Error('Invalid backup file'); } await services.importUserDataBackup(backup); window.parent.postMessage( { type: MessageType.BACKUP_IMPORTED, messageId: event.data.messageId }, event.origin ); } catch (e) { const errorMsg = `Failed to import backup: ${e}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } window.removeEventListener('message', handleMessage); window.addEventListener('message', handleMessage); async function handleMessage(event: MessageEvent) { try { switch (event.data.type) { case MessageType.REQUEST_LINK: await handleRequestLink(event); break; case MessageType.GET_MY_PROCESSES: await handleGetMyProcesses(event); break; case MessageType.GET_PROCESSES: await handleGetProcesses(event); break; case MessageType.RETRIEVE_DATA: await handleDecryptState(event); break; case MessageType.VALIDATE_TOKEN: await handleValidateToken(event); break; case MessageType.RENEW_TOKEN: await handleRenewToken(event); break; case MessageType.GET_PAIRING_ID: await handleGetPairingId(event); break; case MessageType.CREATE_PROCESS: await handleCreateProcess(event); break; case MessageType.NOTIFY_UPDATE: await handleNotifyUpdate(event); break; case MessageType.VALIDATE_STATE: await handleValidateState(event); break; case MessageType.UPDATE_PROCESS: await handleUpdateProcess(event); break; case MessageType.DECODE_PUBLIC_DATA: await handleDecodePublicData(event); break; case MessageType.HASH_VALUE: await handleHashValue(event); break; case MessageType.GET_MERKLE_PROOF: await handleGetMerkleProof(event); break; case MessageType.VALIDATE_MERKLE_PROOF: await handleValidateMerkleProof(event); break; case MessageType.EXPORT_BACKUP: await handleExportBackup(event); break; case MessageType.IMPORT_BACKUP: await handleImportBackup(event); break; default: console.warn(`Unhandled message type: ${event.data.type}`); } } catch (error) { const errorMsg = `Error handling message: ${error}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } window.parent.postMessage( { type: MessageType.LISTENING }, '*' ); } async function cleanPage() { const container = document.querySelector('#containerId'); if (container) container.innerHTML = ''; } async function injectHeader() { const headerContainer = document.getElementById('header-container'); if (headerContainer) { const headerHtml = await fetch('/src/components/header/header.html').then((res) => res.text()); headerContainer.innerHTML = headerHtml; const script = document.createElement('script'); script.src = '/src/components/header/header.ts'; script.type = 'module'; document.head.appendChild(script); initHeader(); } } (window as any).navigate = navigate; document.addEventListener('navigate', ((e: Event) => { const event = e as CustomEvent<{page: string, processId?: string}>; if (event.detail.page === 'chat') { const container = document.querySelector('.container'); if (container) container.innerHTML = ''; //initChat(); const chatElement = document.querySelector('chat-element'); if (chatElement) { chatElement.setAttribute('process-id', event.detail.processId || ''); } } }));